| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  | # Copyright 2009 Brian Quinlan. All Rights Reserved. | 
					
						
							|  |  |  | # Licensed to PSF under a Contributor Agreement. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | """Implements ThreadPoolExecutor.""" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | __author__ = 'Brian Quinlan (brian@sweetapp.com)' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from concurrent.futures import _base | 
					
						
							| 
									
										
										
										
											2017-06-21 23:41:13 -07:00
										 |  |  | import itertools | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  | import queue | 
					
						
							|  |  |  | import threading | 
					
						
							| 
									
										
										
										
											2020-04-10 17:46:36 +03:00
										 |  |  | import types | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  | import weakref | 
					
						
							| 
									
										
										
										
											2014-09-02 10:39:18 -07:00
										 |  |  | import os | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2011-03-26 19:29:44 +01:00
										 |  |  | _threads_queues = weakref.WeakKeyDictionary() | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  | _shutdown = False | 
					
						
							| 
									
										
										
										
											2019-06-28 11:54:52 -07:00
										 |  |  | # Lock that ensures that new workers are not created while the interpreter is | 
					
						
							|  |  |  | # shutting down. Must be held while mutating _threads_queues and _shutdown. | 
					
						
							|  |  |  | _global_shutdown_lock = threading.Lock() | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | def _python_exit(): | 
					
						
							|  |  |  |     global _shutdown | 
					
						
							| 
									
										
										
										
											2019-06-28 11:54:52 -07:00
										 |  |  |     with _global_shutdown_lock: | 
					
						
							|  |  |  |         _shutdown = True | 
					
						
							| 
									
										
										
										
											2011-03-26 19:29:44 +01:00
										 |  |  |     items = list(_threads_queues.items()) | 
					
						
							|  |  |  |     for t, q in items: | 
					
						
							|  |  |  |         q.put(None) | 
					
						
							|  |  |  |     for t, q in items: | 
					
						
							|  |  |  |         t.join() | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-27 15:31:22 -04:00
										 |  |  | # Register for `_python_exit()` to be called just before joining all | 
					
						
							|  |  |  | # non-daemon threads. This is used instead of `atexit.register()` for | 
					
						
							|  |  |  | # compatibility with subinterpreters, which no longer support daemon threads. | 
					
						
							|  |  |  | # See bpo-39812 for context. | 
					
						
							|  |  |  | threading._register_atexit(_python_exit) | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-20 20:30:19 +02:00
										 |  |  | # At fork, reinitialize the `_global_shutdown_lock` lock in the child process | 
					
						
							|  |  |  | if hasattr(os, 'register_at_fork'): | 
					
						
							|  |  |  |     os.register_at_fork(before=_global_shutdown_lock.acquire, | 
					
						
							|  |  |  |                         after_in_child=_global_shutdown_lock._at_fork_reinit, | 
					
						
							|  |  |  |                         after_in_parent=_global_shutdown_lock.release) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-11-04 11:05:49 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-16 16:50:46 -06:00
										 |  |  | class WorkerContext: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def prepare(cls, initializer, initargs): | 
					
						
							|  |  |  |         if initializer is not None: | 
					
						
							|  |  |  |             if not callable(initializer): | 
					
						
							|  |  |  |                 raise TypeError("initializer must be a callable") | 
					
						
							|  |  |  |         def create_context(): | 
					
						
							|  |  |  |             return cls(initializer, initargs) | 
					
						
							|  |  |  |         def resolve_task(fn, args, kwargs): | 
					
						
							|  |  |  |             return (fn, args, kwargs) | 
					
						
							|  |  |  |         return create_context, resolve_task | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, initializer, initargs): | 
					
						
							|  |  |  |         self.initializer = initializer | 
					
						
							|  |  |  |         self.initargs = initargs | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def initialize(self): | 
					
						
							|  |  |  |         if self.initializer is not None: | 
					
						
							|  |  |  |             self.initializer(*self.initargs) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def finalize(self): | 
					
						
							|  |  |  |         pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def run(self, task): | 
					
						
							|  |  |  |         fn, args, kwargs = task | 
					
						
							|  |  |  |         return fn(*args, **kwargs) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-26 08:48:40 +03:00
										 |  |  | class _WorkItem: | 
					
						
							| 
									
										
										
										
											2024-10-16 16:50:46 -06:00
										 |  |  |     def __init__(self, future, task): | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  |         self.future = future | 
					
						
							| 
									
										
										
										
											2024-10-16 16:50:46 -06:00
										 |  |  |         self.task = task | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-16 16:50:46 -06:00
										 |  |  |     def run(self, ctx): | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  |         if not self.future.set_running_or_notify_cancel(): | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         try: | 
					
						
							| 
									
										
										
										
											2024-10-16 16:50:46 -06:00
										 |  |  |             result = ctx.run(self.task) | 
					
						
							| 
									
										
										
										
											2017-08-22 16:50:42 +02:00
										 |  |  |         except BaseException as exc: | 
					
						
							|  |  |  |             self.future.set_exception(exc) | 
					
						
							|  |  |  |             # Break a reference cycle with the exception 'exc' | 
					
						
							|  |  |  |             self = None | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  |         else: | 
					
						
							|  |  |  |             self.future.set_result(result) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-10 17:46:36 +03:00
										 |  |  |     __class_getitem__ = classmethod(types.GenericAlias) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-11-04 11:05:49 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-16 16:50:46 -06:00
										 |  |  | def _worker(executor_reference, ctx, work_queue): | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         ctx.initialize() | 
					
						
							|  |  |  |     except BaseException: | 
					
						
							|  |  |  |         _base.LOGGER.critical('Exception in initializer:', exc_info=True) | 
					
						
							|  |  |  |         executor = executor_reference() | 
					
						
							|  |  |  |         if executor is not None: | 
					
						
							|  |  |  |             executor._initializer_failed() | 
					
						
							|  |  |  |         return | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  |     try: | 
					
						
							|  |  |  |         while True: | 
					
						
							| 
									
										
										
										
											2023-05-26 08:48:40 +03:00
										 |  |  |             try: | 
					
						
							|  |  |  |                 work_item = work_queue.get_nowait() | 
					
						
							|  |  |  |             except queue.Empty: | 
					
						
							|  |  |  |                 # attempt to increment idle count if queue is empty | 
					
						
							| 
									
										
										
										
											2019-05-22 14:29:58 -07:00
										 |  |  |                 executor = executor_reference() | 
					
						
							|  |  |  |                 if executor is not None: | 
					
						
							|  |  |  |                     executor._idle_semaphore.release() | 
					
						
							|  |  |  |                 del executor | 
					
						
							| 
									
										
										
										
											2023-05-26 08:48:40 +03:00
										 |  |  |                 work_item = work_queue.get(block=True) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if work_item is not None: | 
					
						
							| 
									
										
										
										
											2024-10-16 16:50:46 -06:00
										 |  |  |                 work_item.run(ctx) | 
					
						
							| 
									
										
										
										
											2023-05-26 08:48:40 +03:00
										 |  |  |                 # Delete references to object. See GH-60488 | 
					
						
							|  |  |  |                 del work_item | 
					
						
							| 
									
										
										
										
											2011-04-12 17:48:46 +02:00
										 |  |  |                 continue | 
					
						
							| 
									
										
										
										
											2019-05-22 14:29:58 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2011-03-26 19:29:44 +01:00
										 |  |  |             executor = executor_reference() | 
					
						
							|  |  |  |             # Exit if: | 
					
						
							|  |  |  |             #   - The interpreter is shutting down OR | 
					
						
							|  |  |  |             #   - The executor that owns the worker has been collected OR | 
					
						
							|  |  |  |             #   - The executor that owns the worker has been shutdown. | 
					
						
							|  |  |  |             if _shutdown or executor is None or executor._shutdown: | 
					
						
							| 
									
										
										
										
											2018-04-10 18:23:14 +01:00
										 |  |  |                 # Flag the executor as shutting down as early as possible if it | 
					
						
							|  |  |  |                 # is not gc-ed yet. | 
					
						
							|  |  |  |                 if executor is not None: | 
					
						
							|  |  |  |                     executor._shutdown = True | 
					
						
							| 
									
										
										
										
											2011-03-26 19:29:44 +01:00
										 |  |  |                 # Notice other workers | 
					
						
							|  |  |  |                 work_queue.put(None) | 
					
						
							|  |  |  |                 return | 
					
						
							|  |  |  |             del executor | 
					
						
							| 
									
										
										
										
											2011-11-11 20:05:50 +01:00
										 |  |  |     except BaseException: | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  |         _base.LOGGER.critical('Exception in worker', exc_info=True) | 
					
						
							| 
									
										
										
										
											2024-10-16 16:50:46 -06:00
										 |  |  |     finally: | 
					
						
							|  |  |  |         ctx.finalize() | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-11-04 11:05:49 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | class BrokenThreadPool(_base.BrokenExecutor): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Raised when a worker thread in a ThreadPoolExecutor failed initializing. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  | class ThreadPoolExecutor(_base.Executor): | 
					
						
							| 
									
										
										
										
											2017-06-21 23:41:13 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-16 16:50:46 -06:00
										 |  |  |     BROKEN = BrokenThreadPool | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-21 23:41:13 -07:00
										 |  |  |     # Used to assign unique thread names when thread_name_prefix is not supplied. | 
					
						
							|  |  |  |     _counter = itertools.count().__next__ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-16 16:50:46 -06:00
										 |  |  |     @classmethod | 
					
						
							|  |  |  |     def prepare_context(cls, initializer, initargs): | 
					
						
							|  |  |  |         return WorkerContext.prepare(initializer, initargs) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-11-04 11:05:49 +01:00
										 |  |  |     def __init__(self, max_workers=None, thread_name_prefix='', | 
					
						
							| 
									
										
										
										
											2024-10-16 16:50:46 -06:00
										 |  |  |                  initializer=None, initargs=(), **ctxkwargs): | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  |         """Initializes a new ThreadPoolExecutor instance.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Args: | 
					
						
							|  |  |  |             max_workers: The maximum number of threads that can be used to | 
					
						
							|  |  |  |                 execute the given calls. | 
					
						
							| 
									
										
										
										
											2016-08-07 10:19:20 -07:00
										 |  |  |             thread_name_prefix: An optional name prefix to give our threads. | 
					
						
							| 
									
										
										
										
											2019-06-15 13:43:10 +02:00
										 |  |  |             initializer: A callable used to initialize worker threads. | 
					
						
							| 
									
										
										
										
											2017-11-04 11:05:49 +01:00
										 |  |  |             initargs: A tuple of arguments to pass to the initializer. | 
					
						
							| 
									
										
										
										
											2024-10-16 16:50:46 -06:00
										 |  |  |             ctxkwargs: Additional arguments to cls.prepare_context(). | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2014-09-02 10:39:18 -07:00
										 |  |  |         if max_workers is None: | 
					
						
							| 
									
										
										
										
											2019-05-28 21:02:52 +09:00
										 |  |  |             # ThreadPoolExecutor is often used to: | 
					
						
							|  |  |  |             # * CPU bound task which releases GIL | 
					
						
							|  |  |  |             # * I/O bound task (which releases GIL, of course) | 
					
						
							|  |  |  |             # | 
					
						
							| 
									
										
										
										
											2023-10-01 03:14:57 +02:00
										 |  |  |             # We use process_cpu_count + 4 for both types of tasks. | 
					
						
							| 
									
										
										
										
											2019-05-28 21:02:52 +09:00
										 |  |  |             # But we limit it to 32 to avoid consuming surprisingly large resource | 
					
						
							|  |  |  |             # on many core machine. | 
					
						
							| 
									
										
										
										
											2023-10-01 03:14:57 +02:00
										 |  |  |             max_workers = min(32, (os.process_cpu_count() or 1) + 4) | 
					
						
							| 
									
										
										
										
											2014-05-17 13:51:10 -07:00
										 |  |  |         if max_workers <= 0: | 
					
						
							|  |  |  |             raise ValueError("max_workers must be greater than 0") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-16 16:50:46 -06:00
										 |  |  |         (self._create_worker_context, | 
					
						
							|  |  |  |          self._resolve_work_item_task, | 
					
						
							|  |  |  |          ) = type(self).prepare_context(initializer, initargs, **ctxkwargs) | 
					
						
							| 
									
										
										
										
											2017-11-04 11:05:49 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  |         self._max_workers = max_workers | 
					
						
							| 
									
										
										
										
											2018-01-18 10:38:03 +01:00
										 |  |  |         self._work_queue = queue.SimpleQueue() | 
					
						
							| 
									
										
										
										
											2019-05-22 14:29:58 -07:00
										 |  |  |         self._idle_semaphore = threading.Semaphore(0) | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  |         self._threads = set() | 
					
						
							| 
									
										
										
										
											2017-11-04 11:05:49 +01:00
										 |  |  |         self._broken = False | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  |         self._shutdown = False | 
					
						
							|  |  |  |         self._shutdown_lock = threading.Lock() | 
					
						
							| 
									
										
										
										
											2017-06-21 23:41:13 -07:00
										 |  |  |         self._thread_name_prefix = (thread_name_prefix or | 
					
						
							|  |  |  |                                     ("ThreadPoolExecutor-%d" % self._counter())) | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-06-05 18:22:31 +03:00
										 |  |  |     def submit(self, fn, /, *args, **kwargs): | 
					
						
							| 
									
										
										
										
											2019-06-28 11:54:52 -07:00
										 |  |  |         with self._shutdown_lock, _global_shutdown_lock: | 
					
						
							| 
									
										
										
										
											2017-11-04 11:05:49 +01:00
										 |  |  |             if self._broken: | 
					
						
							| 
									
										
										
										
											2024-10-16 16:50:46 -06:00
										 |  |  |                 raise self.BROKEN(self._broken) | 
					
						
							| 
									
										
										
										
											2017-11-04 11:05:49 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  |             if self._shutdown: | 
					
						
							|  |  |  |                 raise RuntimeError('cannot schedule new futures after shutdown') | 
					
						
							| 
									
										
										
										
											2018-04-10 18:23:14 +01:00
										 |  |  |             if _shutdown: | 
					
						
							| 
									
										
										
										
											2018-11-05 16:20:25 +02:00
										 |  |  |                 raise RuntimeError('cannot schedule new futures after ' | 
					
						
							| 
									
										
										
										
											2018-04-10 18:23:14 +01:00
										 |  |  |                                    'interpreter shutdown') | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |             f = _base.Future() | 
					
						
							| 
									
										
										
										
											2024-10-16 16:50:46 -06:00
										 |  |  |             task = self._resolve_work_item_task(fn, args, kwargs) | 
					
						
							|  |  |  |             w = _WorkItem(f, task) | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |             self._work_queue.put(w) | 
					
						
							|  |  |  |             self._adjust_thread_count() | 
					
						
							|  |  |  |             return f | 
					
						
							|  |  |  |     submit.__doc__ = _base.Executor.submit.__doc__ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _adjust_thread_count(self): | 
					
						
							| 
									
										
										
										
											2019-05-22 14:29:58 -07:00
										 |  |  |         # if idle threads are available, don't spin new threads | 
					
						
							|  |  |  |         if self._idle_semaphore.acquire(timeout=0): | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2011-03-26 19:29:44 +01:00
										 |  |  |         # When the executor gets lost, the weakref callback will wake up | 
					
						
							|  |  |  |         # the worker threads. | 
					
						
							|  |  |  |         def weakref_cb(_, q=self._work_queue): | 
					
						
							|  |  |  |             q.put(None) | 
					
						
							| 
									
										
										
										
											2019-05-22 14:29:58 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-07 10:19:20 -07:00
										 |  |  |         num_threads = len(self._threads) | 
					
						
							|  |  |  |         if num_threads < self._max_workers: | 
					
						
							|  |  |  |             thread_name = '%s_%d' % (self._thread_name_prefix or self, | 
					
						
							|  |  |  |                                      num_threads) | 
					
						
							|  |  |  |             t = threading.Thread(name=thread_name, target=_worker, | 
					
						
							| 
									
										
										
										
											2011-03-26 19:29:44 +01:00
										 |  |  |                                  args=(weakref.ref(self, weakref_cb), | 
					
						
							| 
									
										
										
										
											2024-10-16 16:50:46 -06:00
										 |  |  |                                        self._create_worker_context(), | 
					
						
							|  |  |  |                                        self._work_queue)) | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  |             t.start() | 
					
						
							|  |  |  |             self._threads.add(t) | 
					
						
							| 
									
										
										
										
											2011-03-26 19:29:44 +01:00
										 |  |  |             _threads_queues[t] = self._work_queue | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-11-04 11:05:49 +01:00
										 |  |  |     def _initializer_failed(self): | 
					
						
							|  |  |  |         with self._shutdown_lock: | 
					
						
							|  |  |  |             self._broken = ('A thread initializer failed, the thread pool ' | 
					
						
							|  |  |  |                             'is not usable anymore') | 
					
						
							|  |  |  |             # Drain work queue and mark pending futures failed | 
					
						
							|  |  |  |             while True: | 
					
						
							|  |  |  |                 try: | 
					
						
							|  |  |  |                     work_item = self._work_queue.get_nowait() | 
					
						
							|  |  |  |                 except queue.Empty: | 
					
						
							|  |  |  |                     break | 
					
						
							|  |  |  |                 if work_item is not None: | 
					
						
							| 
									
										
										
										
											2024-10-16 16:50:46 -06:00
										 |  |  |                     work_item.future.set_exception(self.BROKEN(self._broken)) | 
					
						
							| 
									
										
										
										
											2017-11-04 11:05:49 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-02 07:49:00 -05:00
										 |  |  |     def shutdown(self, wait=True, *, cancel_futures=False): | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  |         with self._shutdown_lock: | 
					
						
							|  |  |  |             self._shutdown = True | 
					
						
							| 
									
										
										
										
											2020-02-02 07:49:00 -05:00
										 |  |  |             if cancel_futures: | 
					
						
							|  |  |  |                 # Drain all work items from the queue, and then cancel their | 
					
						
							|  |  |  |                 # associated futures. | 
					
						
							|  |  |  |                 while True: | 
					
						
							|  |  |  |                     try: | 
					
						
							|  |  |  |                         work_item = self._work_queue.get_nowait() | 
					
						
							|  |  |  |                     except queue.Empty: | 
					
						
							|  |  |  |                         break | 
					
						
							|  |  |  |                     if work_item is not None: | 
					
						
							|  |  |  |                         work_item.future.cancel() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # Send a wake-up to prevent threads calling | 
					
						
							|  |  |  |             # _work_queue.get(block=True) from permanently blocking. | 
					
						
							| 
									
										
										
										
											2011-03-26 19:29:44 +01:00
										 |  |  |             self._work_queue.put(None) | 
					
						
							| 
									
										
										
										
											2010-09-18 22:35:02 +00:00
										 |  |  |         if wait: | 
					
						
							|  |  |  |             for t in self._threads: | 
					
						
							|  |  |  |                 t.join() | 
					
						
							|  |  |  |     shutdown.__doc__ = _base.Executor.shutdown.__doc__ |