corrade-vassal – Blame information for rev 1

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 vero 1 /*
2 * CVS identifier:
3 *
4 * $Id: ThreadPool.java,v 1.9 2002/05/22 15:00:55 grosbois Exp $
5 *
6 * Class: ThreadPool
7 *
8 * Description: A pool of threads
9 *
10 *
11 *
12 * COPYRIGHT:
13 *
14 * This software module was originally developed by Raphaël Grosbois and
15 * Diego Santa Cruz (Swiss Federal Institute of Technology-EPFL); Joel
16 * Askelöf (Ericsson Radio Systems AB); and Bertrand Berthelot, David
17 * Bouchard, Félix Henry, Gerard Mozelle and Patrice Onno (Canon Research
18 * Centre France S.A) in the course of development of the JPEG2000
19 * standard as specified by ISO/IEC 15444 (JPEG 2000 Standard). This
20 * software module is an implementation of a part of the JPEG 2000
21 * Standard. Swiss Federal Institute of Technology-EPFL, Ericsson Radio
22 * Systems AB and Canon Research Centre France S.A (collectively JJ2000
23 * Partners) agree not to assert against ISO/IEC and users of the JPEG
24 * 2000 Standard (Users) any of their rights under the copyright, not
25 * including other intellectual property rights, for this software module
26 * with respect to the usage by ISO/IEC and Users of this software module
27 * or modifications thereof for use in hardware or software products
28 * claiming conformance to the JPEG 2000 Standard. Those intending to use
29 * this software module in hardware or software products are advised that
30 * their use may infringe existing patents. The original developers of
31 * this software module, JJ2000 Partners and ISO/IEC assume no liability
32 * for use of this software module or modifications thereof. No license
33 * or right to this software module is granted for non JPEG 2000 Standard
34 * conforming products. JJ2000 Partners have full right to use this
35 * software module for his/her own purpose, assign or donate this
36 * software module to any third party and to inhibit third parties from
37 * using this software module for non JPEG 2000 Standard conforming
38 * products. This copyright notice must be included in all copies or
39 * derivative works of this software module.
40 *
41 * Copyright (c) 1999/2000 JJ2000 Partners.
42 * */
43 using System;
44 namespace CSJ2K.j2k.util
45 {
46  
47 /// <summary> This class implements a thread pool. The thread pool contains a set of
48 /// threads which can be given work to do.
49 ///
50 /// <P>If the Java Virtual Machine (JVM) uses native threads, then the
51 /// different threads will be able to execute in different processors in
52 /// parallel on multiprocessors machines. However, under some JVMs and
53 /// operating systems using native threads is not sufficient to allow the JVM
54 /// access to multiple processors. This is the case when native threads are
55 /// implemented using POSIX threads on lightweight processes
56 /// (i.e. PTHREAD_SCOPE_PROCESS sopce scheduling), which is the case on most
57 /// UNIX operating systems. In order to do provide access to multiple
58 /// processors it is necessary to set the concurrency level to the number of
59 /// processors or slightly higher. This can be achieved by setting the Java
60 /// system property with the name defined by CONCURRENCY_PROP_NAME to some
61 /// non-negative number. This will make use of the 'NativeServices' class and
62 /// supporting native libraries. See 'NativeServices' for details. See
63 /// 'CONCURRENCY_PROP_NAME' for the name of the property.
64 ///
65 /// <P>Initially the thread pool contains a user specified number of idle
66 /// threads. Idle threads can be given a target which is run. While running the
67 /// target the thread temporarily leaves the idle list. When the target
68 /// finishes, it joins the idle list again, waiting for a new target. When a
69 /// target is finished a thread can be notified on a particular object that is
70 /// given as a lock.
71 ///
72 /// <P>Jobs can be submitted using Runnable interfaces, using the 'runTarget()'
73 /// methods. When the job is submitted, an idle thread will be obtained, the
74 /// 'run()' method of the 'Runnable' interface will be executed and when it
75 /// completes the thread will be returned to the idle list. In general the
76 /// 'run()' method should complete in a rather short time, so that the threds
77 /// of the pool are not starved.
78 ///
79 /// <P>If using the non-asynchronous calls to 'runTarget()', it is important
80 /// that any target's 'run()' method, or any method called from it, does not
81 /// use non-asynchronous calls to 'runTarget()' on the same thread pool where
82 /// it was started. Otherwise this could create a dead-lock when there are not
83 /// enough idle threads.
84 ///
85 /// <P>The pool also has a global error and runtime exception condition (one
86 /// for 'Error' and one for 'RuntimeException'). If a target's 'run()' method
87 /// throws an 'Error' or 'RuntimeException' the corresponding exception
88 /// condition is set and the exception object saved. In any subsequent call to
89 /// 'checkTargetErrors()' the saved exception object is thrown. Likewise, if a
90 /// target's 'run()' method throws any other subclass of 'Throwable' a new
91 /// 'RuntimeException' is created and saved. It will be thrown on a subsequent
92 /// call to 'checkTargetErrors()'. If more than one exception occurs between
93 /// calls to 'checkTargetErrors()' only the last one is saved. Any 'Error'
94 /// condition has precedence on all 'RuntimeException' conditions. The threads
95 /// in the pool are unaffected by any exceptions thrown by targets.
96 ///
97 /// <P>The only exception to the above is the 'ThreadDeath' exception. If a
98 /// target's 'run()' method throws the 'ThreadDeath' exception a warning
99 /// message is printed and the exception is propagated, which will terminate
100 /// the thread in which it occurs. This could lead to instabilities of the
101 /// pool. The 'ThreadDeath' exception should never be thrown by the program. It
102 /// is thrown by the Java(TM) Virtual Machine when Thread.stop() is
103 /// called. This method is deprecated and should never be called.
104 ///
105 /// <P>All the threads in the pool are "daemon" threads and will automatically
106 /// terminate when no daemon threads are running.
107 ///
108 /// </summary>
109 /// <seealso cref="NativeServices">
110 ///
111 /// </seealso>
112 /// <seealso cref="CONCURRENCY_PROP_NAME">
113 ///
114 /// </seealso>
115 /// <seealso cref="Runnable">
116 ///
117 /// </seealso>
118 /// <seealso cref="Thread">
119 ///
120 /// </seealso>
121 /// <seealso cref="Error">
122 ///
123 /// </seealso>
124 /// <seealso cref="RuntimeException">
125 ///
126 ///
127 /// </seealso>
128 public class ThreadPool
129 {
130 /// <summary> Returns the size of the pool. That is the number of threads in this
131 /// pool (idle + busy).
132 ///
133 /// </summary>
134 /// <returns> The pool's size.
135 ///
136 ///
137 /// </returns>
138 virtual public int Size
139 {
140 get
141 {
142 return idle.Length;
143 }
144  
145 }
146  
147 /// <summary>The name of the property that sets the concurrency level:
148 /// jj2000.j2k.util.ThreadPool.concurrency
149 /// </summary>
150 public const System.String CONCURRENCY_PROP_NAME = "jj2000.j2k.util.ThreadPool.concurrency";
151  
152 /// <summary>The array of idle threads and the lock for the manipulation of the
153 /// idle thread list.
154 /// </summary>
155 private ThreadPoolThread[] idle;
156  
157 /// <summary>The number of idle threads </summary>
158 private int nidle;
159  
160 /// <summary>The name of the pool </summary>
161 private System.String poolName;
162  
163 /// <summary>The priority for the pool </summary>
164 private int poolPriority;
165  
166 /// <summary>The last error thrown by a target. Null if none </summary>
167 // NOTE: needs to be volatile, so that only one copy exits in memory
168 private volatile System.ApplicationException targetE;
169  
170 /// <summary>The last runtime exception thrown by a target. Null if none </summary>
171 // NOTE: needs to be volatile, so that only one copy exits in memory
172 private volatile System.SystemException targetRE;
173  
174 //UPGRADE_NOTE: Field 'EnclosingInstance' was added to class 'ThreadPoolThread' to access its enclosing instance. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1019'"
175 /// <summary> The threads that are managed by the pool.
176 ///
177 /// </summary>
178 internal class ThreadPoolThread:SupportClass.ThreadClass
179 {
180 private void InitBlock(ThreadPool enclosingInstance)
181 {
182 this.enclosingInstance = enclosingInstance;
183 }
184 private ThreadPool enclosingInstance;
185 public ThreadPool Enclosing_Instance
186 {
187 get
188 {
189 return enclosingInstance;
190 }
191  
192 }
193 private IThreadRunnable target;
194 private System.Object lock_Renamed;
195 private bool doNotifyAll;
196  
197 /// <summary> Creates a ThreadPoolThread object, setting its name according to
198 /// the given 'idx', daemon type and the priority to the one of the
199 /// pool.
200 ///
201 /// </summary>
202 /// <param name="idx">The index of this thread in the pool
203 ///
204 /// </param>
205 /// <param name="name">The name of the thread
206 ///
207 /// </param>
208 public ThreadPoolThread(ThreadPool enclosingInstance, int idx, System.String name):base(name)
209 {
210 InitBlock(enclosingInstance);
211 IsBackground = true;
212 //UPGRADE_TODO: The differences in the type of parameters for method 'java.lang.Thread.setPriority' may cause compilation errors. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1092'"
213 Priority = (System.Threading.ThreadPriority) Enclosing_Instance.poolPriority;
214 }
215  
216 /// <summary> The method that is run by the thread. This method first joins the
217 /// idle state in the pool and then enters an infinite loop. In this
218 /// loop it waits until a target to run exists and runs it. Once the
219 /// target's run() method is done it re-joins the idle state and
220 /// notifies the waiting lock object, if one exists.
221 ///
222 /// <P>An interrupt on this thread has no effect other than forcing a
223 /// check on the target. Normally the target is checked every time the
224 /// thread is woken up by notify, no interrupts should be done.
225 ///
226 /// <P>Any exception thrown by the target's 'run()' method is catched
227 /// and this thread is not affected, except for 'ThreadDeath'. If a
228 /// 'ThreadDeath' exception is catched a warning message is printed by
229 /// the 'FacilityManager' and the exception is propagated up. For
230 /// exceptions which are subclasses of 'Error' or 'RuntimeException'
231 /// the corresponding error condition is set and this thread is not
232 /// affected. For any other exceptions a new 'RuntimeException' is
233 /// created and the error condition is set, this thread is not affected.
234 ///
235 /// </summary>
236 override public void Run()
237 {
238 // Join the idle threads list
239 Enclosing_Instance.putInIdleList(this);
240 // Permanently lock the object while running so that target can
241 // not be changed until we are waiting again. While waiting for a
242 // target the lock is released.
243 lock (this)
244 {
245 while (true)
246 {
247 // Wait until we get a target
248 while (target == null)
249 {
250 try
251 {
252 System.Threading.Monitor.Wait(this);
253 }
254 catch (System.Threading.ThreadInterruptedException)
255 {
256 }
257 }
258 // Run the target and catch all possible errors
259 try
260 {
261 target.Run();
262 }
263 //UPGRADE_NOTE: Exception 'java.lang.ThreadDeath' was converted to 'System.ApplicationException' which has different behavior. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1100'"
264 catch (System.Threading.ThreadAbortException td)
265 {
266 // We have been instructed to abruptly terminate
267 // the thread, which should never be done. This can
268 // cause another thread, or the system, to lock.
269 FacilityManager.getMsgLogger().printmsg(CSJ2K.j2k.util.MsgLogger_Fields.WARNING, "Thread.stop() called on a ThreadPool " + "thread or ThreadDeath thrown. This is " + "deprecated. Lock-up might occur.");
270 throw td;
271 }
272 catch (System.ApplicationException e)
273 {
274 Enclosing_Instance.targetE = e;
275 }
276 catch (System.SystemException re)
277 {
278 Enclosing_Instance.targetRE = re;
279 }
280 //UPGRADE_NOTE: Exception 'java.lang.Throwable' was converted to 'System.Exception' which has different behavior. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1100'"
281 catch (System.Exception)
282 {
283 // A totally unexpected error has occurred
284 // (Thread.stop(Throwable) has been used, which should
285 // never be.
286 Enclosing_Instance.targetRE = new System.SystemException("Unchecked exception " + "thrown by target's " + "run() method in pool " + Enclosing_Instance.poolName + ".");
287 }
288 // Join idle threads
289 Enclosing_Instance.putInIdleList(this);
290 // Release the target and notify lock (i.e. wakeup)
291 target = null;
292 if (lock_Renamed != null)
293 {
294 lock (lock_Renamed)
295 {
296 if (doNotifyAll)
297 {
298 System.Threading.Monitor.PulseAll(lock_Renamed);
299 }
300 else
301 {
302 System.Threading.Monitor.Pulse(lock_Renamed);
303 }
304 }
305 }
306 }
307 }
308 }
309  
310 /// <summary> Assigns a target to this thread, with an optional notify lock and a
311 /// notify mode. The another target is currently running the method
312 /// will block until it terminates. After setting the new target the
313 /// runner thread will be wakenup and execytion will start.
314 ///
315 /// </summary>
316 /// <param name="target">The runnable object containing the 'run()' method to
317 /// run.
318 ///
319 /// </param>
320 /// <param name="lock">An object on which notify will be called once the
321 /// target's run method has finished. A thread to be notified should be
322 /// waiting on that object. If null no thread is notified.
323 ///
324 /// </param>
325 /// <param name="notifyAll">If true 'notifyAll()', instead of 'notify()', will
326 /// be called on tghe lock.
327 ///
328 /// </param>
329 //UPGRADE_NOTE: Synchronized keyword was removed from method 'setTarget'. Lock expression was added. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1027'"
330 internal virtual void setTarget(IThreadRunnable target, System.Object lock_Renamed, bool notifyAll)
331 {
332 lock (this)
333 {
334 // Set the target
335 this.target = target;
336 this.lock_Renamed = lock_Renamed;
337 doNotifyAll = notifyAll;
338 // Wakeup the thread
339 System.Threading.Monitor.Pulse(this);
340 }
341 }
342 }
343  
344 /// <summary> Creates a new thread pool of the given size, thread priority and pool
345 /// name.
346 ///
347 /// <P>If the Java system property of the name defined by
348 /// 'CONCURRENCY_PROP_NAME' is set, then an attempt will be made to load
349 /// the library that supports concurrency setting (see
350 /// 'NativeServices'). If that succeds the concurrency level will be set to
351 /// the specified value. Otherwise a warning is printed.
352 ///
353 /// </summary>
354 /// <param name="size">The size of the pool (number of threads to create in the
355 /// pool).
356 ///
357 /// </param>
358 /// <param name="priority">The priority to give to the threads in the pool. If
359 /// less than 'Thread.MIN_PRIORITY' it will be the same as the priority of
360 /// the calling thread.
361 ///
362 /// </param>
363 /// <param name="name">The name of the pool. If null a default generic name is
364 /// chosen.
365 ///
366 /// </param>
367 /// <seealso cref="NativeServices">
368 ///
369 /// </seealso>
370 /// <seealso cref="CONCURRENCY_PROP_NAME">
371 ///
372 /// </seealso>
373 public ThreadPool(int size, int priority, System.String name)
374 {
375 int i;
376 ThreadPoolThread t;
377 //System.String prop;
378 //int clevel;
379  
380 // Initialize variables checking for special cases
381 if (size <= 0)
382 {
383 throw new System.ArgumentException("Pool must be of positive size");
384 }
385 if (priority < (int) System.Threading.ThreadPriority.Lowest)
386 {
387 poolPriority = (System.Int32) SupportClass.ThreadClass.Current().Priority;
388 }
389 else
390 {
391 poolPriority = (priority < (int) System.Threading.ThreadPriority.Highest)?priority:(int) System.Threading.ThreadPriority.Highest;
392 }
393 if (name == null)
394 {
395 poolName = "Anonymous ThreadPool";
396 }
397 else
398 {
399 poolName = name;
400 }
401  
402 // Allocate internal variables
403 idle = new ThreadPoolThread[size];
404 nidle = 0;
405  
406 // Create and start the threads
407 for (i = 0; i < size; i++)
408 {
409 t = new ThreadPoolThread(this, i, poolName + "-" + i);
410 t.Start();
411 }
412 }
413  
414 /// <summary> Runs the run method of the specified target in an idle thread of this
415 /// pool. When the target's run method completes, the thread waiting on the
416 /// lock object is notified, if any. If there is currently no idle thread
417 /// the method will block until a thread of the pool becomes idle or the
418 /// calling thread is interrupted.
419 ///
420 /// <P>This method is the same as <tt>runTarget(t,l,true,false)</tt>.
421 ///
422 /// </summary>
423 /// <param name="t">The target. The 'run()' method of this object will be run in
424 /// an idle thread of the pool.
425 ///
426 /// </param>
427 /// <param name="l">The lock object. A thread waiting on the lock of the 'l'
428 /// object will be notified, through the 'notify()' call, when the target's
429 /// run method completes. If null no thread is notified.
430 ///
431 /// </param>
432 /// <returns> True if the target was submitted to some thread. False if no
433 /// idle thread could be found and the target was not submitted for
434 /// execution.
435 ///
436 ///
437 /// </returns>
438 public virtual bool runTarget(IThreadRunnable t, System.Object l)
439 {
440 return runTarget(t, l, false, false);
441 }
442  
443 /// <summary> Runs the run method of the specified target in an idle thread of this
444 /// pool. When the target's run method completes, the thread waiting on the
445 /// lock object is notified, if any. If there is currently no idle thread
446 /// and the asynchronous mode is not used the method will block until a
447 /// thread of the pool becomes idle or the calling thread is
448 /// interrupted. If the asynchronous mode is used then the method will not
449 /// block and will return false.
450 ///
451 /// <P>This method is the same as <tt>runTarget(t,l,async,false)</tt>.
452 ///
453 /// </summary>
454 /// <param name="t">The target. The 'run()' method of this object will be run in
455 /// an idle thread of the pool.
456 ///
457 /// </param>
458 /// <param name="l">The lock object. A thread waiting on the lock of the 'l'
459 /// object will be notified, through the 'notify()' call, when the target's
460 /// run method completes. If null no thread is notified.
461 ///
462 /// </param>
463 /// <param name="async">If true the asynchronous mode will be used.
464 ///
465 /// </param>
466 /// <returns> True if the target was submitted to some thread. False if no
467 /// idle thread could be found and the target was not submitted for
468 /// execution.
469 ///
470 ///
471 /// </returns>
472 public virtual bool runTarget(IThreadRunnable t, System.Object l, bool async)
473 {
474 return runTarget(t, l, async, false);
475 }
476  
477 /// <summary> Runs the run method of the specified target in an idle thread of this
478 /// pool. When the target's run method completes, the thread waiting on the
479 /// lock object is notified, if any. If there is currently no idle thread
480 /// and the asynchronous mode is not used the method will block until a
481 /// thread of the pool becomes idle or the calling thread is
482 /// interrupted. If the asynchronous mode is used then the method will not
483 /// block and will return false.
484 ///
485 /// </summary>
486 /// <param name="t">The target. The 'run()' method of this object will be run in
487 /// an idle thread of the pool.
488 ///
489 /// </param>
490 /// <param name="l">The lock object. A thread waiting on the lock of the 'l'
491 /// object will be notified, through the 'notify()' call, when the target's
492 /// run method completes. If null no thread is notified.
493 ///
494 /// </param>
495 /// <param name="async">If true the asynchronous mode will be used.
496 ///
497 /// </param>
498 /// <param name="notifyAll">If true, threads waiting on the lock of the 'l' object
499 /// will be notified trough the 'notifyAll()' instead of the normal
500 /// 'notify()' call. This is not normally needed.
501 ///
502 /// </param>
503 /// <returns> True if the target was submitted to some thread. False if no
504 /// idle thread could be found and the target was not submitted for
505 /// execution.
506 ///
507 ///
508 /// </returns>
509 public virtual bool runTarget(IThreadRunnable t, System.Object l, bool async, bool notifyAll)
510 {
511 ThreadPoolThread runner; // The thread to run the target
512  
513 // Get a thread to run
514 runner = getIdle(async);
515 // If no runner return failure
516 if (runner == null)
517 return false;
518 // Set the runner
519 runner.setTarget(t, l, notifyAll);
520 return true;
521 }
522  
523 /// <summary> Checks that no error or runtime exception in any target have occurred
524 /// so far. If an error or runtime exception has occurred in a target's run
525 /// method they are thrown by this method.
526 ///
527 /// </summary>
528 /// <exception cref="Error">If an error condition has been thrown by a target
529 /// 'run()' method.
530 ///
531 /// </exception>
532 /// <exception cref="RuntimeException">If a runtime exception has been thrown by a
533 /// target 'run()' method.
534 ///
535 /// </exception>
536 public virtual void checkTargetErrors()
537 {
538 // Check for Error
539 if (targetE != null)
540 throw targetE;
541 // Check for RuntimeException
542 if (targetRE != null)
543 throw targetRE;
544 }
545  
546 /// <summary> Clears the current target error conditions, if any. Note that a thread
547 /// in the pool might have set the error conditions since the last check
548 /// and that those error conditions will be lost. Likewise, before
549 /// returning from this method another thread might set the error
550 /// conditions. There is no guarantee that no error conditions exist when
551 /// returning from this method.
552 ///
553 /// <P>In order to ensure that no error conditions exist when returning
554 /// from this method cooperation from the targets and the thread using this
555 /// pool is necessary (i.e. currently no targets running or waiting to
556 /// run).
557 ///
558 /// </summary>
559 public virtual void clearTargetErrors()
560 {
561 // Clear the error and runtime exception conditions
562 targetE = null;
563 targetRE = null;
564 }
565  
566 /// <summary> Puts the thread 't' in the idle list. The thread 't' should be in fact
567 /// idle and ready to accept a new target when it joins the idle list.
568 ///
569 /// <P> An idle thread that is already in the list should never add itself
570 /// to the list before it is removed. For efficiency reasons there is no
571 /// check to see if the thread is already in the list of idle threads.
572 ///
573 /// <P> If the idle list was empty 'notify()' will be called on the 'idle'
574 /// array, to wake up a thread that might be waiting (within the
575 /// 'getIdle()' method) on an idle thread to become available.
576 ///
577 /// </summary>
578 /// <param name="t">The thread to put in the idle list.
579 ///
580 /// </param>
581 private void putInIdleList(ThreadPoolThread t)
582 {
583 // NOTE: if already in idle => catastrophe! (should be OK since //
584 // this is private method)
585 // Lock the idle array to avoid races with 'getIdle()'
586 lock (idle)
587 {
588 idle[nidle] = t;
589 nidle++;
590 // If idle array was empty wakeup any waiting threads.
591 if (nidle == 1)
592 System.Threading.Monitor.Pulse(idle);
593 }
594 }
595  
596 /// <summary> Returns and idle thread and removes it from the list of idle
597 /// threads. In asynchronous mode it will immediately return an idle
598 /// thread, or null if none is available. In non-asynchronous mode it will
599 /// block until a thread of the pool becomes idle or the calling thread is
600 /// interrupted.
601 ///
602 /// <P>If in non-asynchronous mode and there are currently no idle threads
603 /// available the calling thread will wait on the 'idle' array lock, until
604 /// notified by 'putInIdleList()' that an idle thread might have become
605 /// available.
606 ///
607 /// </summary>
608 /// <param name="async">If true asynchronous mode is used.
609 ///
610 /// </param>
611 /// <returns> An idle thread of the pool, that has been removed from the idle
612 /// list, or null if none is available.
613 ///
614 /// </returns>
615 private ThreadPoolThread getIdle(bool async)
616 {
617 // Lock the idle array to avoid races with 'putInIdleList()'
618 lock (idle)
619 {
620 if (async)
621 {
622 // In asynchronous mode just return null if no idle thread
623 if (nidle == 0)
624 return null;
625 }
626 else
627 {
628 // In synchronous mode wait until a thread becomes idle
629 while (nidle == 0)
630 {
631 try
632 {
633 System.Threading.Monitor.Wait(idle);
634 }
635 catch (System.Threading.ThreadInterruptedException)
636 {
637 // If we were interrupted just return null
638 return null;
639 }
640 }
641 }
642 // Decrease the idle count and return one of the idle threads
643 nidle--;
644 return idle[nidle];
645 }
646 }
647 }
648 }