Commit c13d454e authored by Antoine Pitrou's avatar Antoine Pitrou

Issue #11635: Don't use polling in worker threads and processes launched by

concurrent.futures.
parent d9faa201
...@@ -66,28 +66,17 @@ import weakref ...@@ -66,28 +66,17 @@ import weakref
# workers to exit when their work queues are empty and then waits until the # workers to exit when their work queues are empty and then waits until the
# threads/processes finish. # threads/processes finish.
_thread_references = set() _threads_queues = weakref.WeakKeyDictionary()
_shutdown = False _shutdown = False
def _python_exit(): def _python_exit():
global _shutdown global _shutdown
_shutdown = True _shutdown = True
for thread_reference in _thread_references: items = list(_threads_queues.items())
thread = thread_reference() for t, q in items:
if thread is not None: q.put(None)
thread.join() for t, q in items:
t.join()
def _remove_dead_thread_references():
"""Remove inactive threads from _thread_references.
Should be called periodically to prevent memory leaks in scenarios such as:
>>> while True:
>>> ... t = ThreadPoolExecutor(max_workers=5)
>>> ... t.map(int, ['1', '2', '3', '4', '5'])
"""
for thread_reference in set(_thread_references):
if thread_reference() is None:
_thread_references.discard(thread_reference)
# Controls how many more calls than processes will be queued in the call queue. # Controls how many more calls than processes will be queued in the call queue.
# A smaller number will mean that processes spend more time idle waiting for # A smaller number will mean that processes spend more time idle waiting for
...@@ -130,11 +119,15 @@ def _process_worker(call_queue, result_queue, shutdown): ...@@ -130,11 +119,15 @@ def _process_worker(call_queue, result_queue, shutdown):
""" """
while True: while True:
try: try:
call_item = call_queue.get(block=True, timeout=0.1) call_item = call_queue.get(block=True)
except queue.Empty: except queue.Empty:
if shutdown.is_set(): if shutdown.is_set():
return return
else: else:
if call_item is None:
# Wake up queue management thread
result_queue.put(None)
return
try: try:
r = call_item.fn(*call_item.args, **call_item.kwargs) r = call_item.fn(*call_item.args, **call_item.kwargs)
except BaseException as e: except BaseException as e:
...@@ -209,14 +202,33 @@ def _queue_manangement_worker(executor_reference, ...@@ -209,14 +202,33 @@ def _queue_manangement_worker(executor_reference,
process workers that they should exit when their work queue is process workers that they should exit when their work queue is
empty. empty.
""" """
nb_shutdown_processes = 0
def shutdown_one_process():
"""Tell a worker to terminate, which will in turn wake us again"""
nonlocal nb_shutdown_processes
call_queue.put(None)
nb_shutdown_processes += 1
while True: while True:
_add_call_item_to_queue(pending_work_items, _add_call_item_to_queue(pending_work_items,
work_ids_queue, work_ids_queue,
call_queue) call_queue)
try: try:
result_item = result_queue.get(block=True, timeout=0.1) result_item = result_queue.get(block=True)
except queue.Empty: except queue.Empty:
pass
else:
if result_item is not None:
work_item = pending_work_items[result_item.work_id]
del pending_work_items[result_item.work_id]
if result_item.exception:
work_item.future.set_exception(result_item.exception)
else:
work_item.future.set_result(result_item.result)
continue
# If we come here, we either got a timeout or were explicitly woken up.
# In either case, check whether we should start shutting down.
executor = executor_reference() executor = executor_reference()
# No more work items can be added if: # No more work items can be added if:
# - The interpreter is shutting down OR # - The interpreter is shutting down OR
...@@ -228,21 +240,18 @@ def _queue_manangement_worker(executor_reference, ...@@ -228,21 +240,18 @@ def _queue_manangement_worker(executor_reference,
if not pending_work_items: if not pending_work_items:
shutdown_process_event.set() shutdown_process_event.set()
while nb_shutdown_processes < len(processes):
shutdown_one_process()
# If .join() is not called on the created processes then # If .join() is not called on the created processes then
# some multiprocessing.Queue methods may deadlock on Mac OS # some multiprocessing.Queue methods may deadlock on Mac OS
# X. # X.
for p in processes: for p in processes:
p.join() p.join()
return return
del executor
else: else:
work_item = pending_work_items[result_item.work_id] # Start shutting down by telling a process it can exit.
del pending_work_items[result_item.work_id] shutdown_one_process()
del executor
if result_item.exception:
work_item.future.set_exception(result_item.exception)
else:
work_item.future.set_result(result_item.result)
_system_limits_checked = False _system_limits_checked = False
_system_limited = None _system_limited = None
...@@ -279,7 +288,6 @@ class ProcessPoolExecutor(_base.Executor): ...@@ -279,7 +288,6 @@ class ProcessPoolExecutor(_base.Executor):
worker processes will be created as the machine has processors. worker processes will be created as the machine has processors.
""" """
_check_system_limits() _check_system_limits()
_remove_dead_thread_references()
if max_workers is None: if max_workers is None:
self._max_workers = multiprocessing.cpu_count() self._max_workers = multiprocessing.cpu_count()
...@@ -304,10 +312,14 @@ class ProcessPoolExecutor(_base.Executor): ...@@ -304,10 +312,14 @@ class ProcessPoolExecutor(_base.Executor):
self._pending_work_items = {} self._pending_work_items = {}
def _start_queue_management_thread(self): def _start_queue_management_thread(self):
# When the executor gets lost, the weakref callback will wake up
# the queue management thread.
def weakref_cb(_, q=self._result_queue):
q.put(None)
if self._queue_management_thread is None: if self._queue_management_thread is None:
self._queue_management_thread = threading.Thread( self._queue_management_thread = threading.Thread(
target=_queue_manangement_worker, target=_queue_manangement_worker,
args=(weakref.ref(self), args=(weakref.ref(self, weakref_cb),
self._processes, self._processes,
self._pending_work_items, self._pending_work_items,
self._work_ids, self._work_ids,
...@@ -316,7 +328,7 @@ class ProcessPoolExecutor(_base.Executor): ...@@ -316,7 +328,7 @@ class ProcessPoolExecutor(_base.Executor):
self._shutdown_process_event)) self._shutdown_process_event))
self._queue_management_thread.daemon = True self._queue_management_thread.daemon = True
self._queue_management_thread.start() self._queue_management_thread.start()
_thread_references.add(weakref.ref(self._queue_management_thread)) _threads_queues[self._queue_management_thread] = self._result_queue
def _adjust_process_count(self): def _adjust_process_count(self):
for _ in range(len(self._processes), self._max_workers): for _ in range(len(self._processes), self._max_workers):
...@@ -339,6 +351,8 @@ class ProcessPoolExecutor(_base.Executor): ...@@ -339,6 +351,8 @@ class ProcessPoolExecutor(_base.Executor):
self._pending_work_items[self._queue_count] = w self._pending_work_items[self._queue_count] = w
self._work_ids.put(self._queue_count) self._work_ids.put(self._queue_count)
self._queue_count += 1 self._queue_count += 1
# Wake up queue management thread
self._result_queue.put(None)
self._start_queue_management_thread() self._start_queue_management_thread()
self._adjust_process_count() self._adjust_process_count()
...@@ -348,8 +362,10 @@ class ProcessPoolExecutor(_base.Executor): ...@@ -348,8 +362,10 @@ class ProcessPoolExecutor(_base.Executor):
def shutdown(self, wait=True): def shutdown(self, wait=True):
with self._shutdown_lock: with self._shutdown_lock:
self._shutdown_thread = True self._shutdown_thread = True
if wait:
if self._queue_management_thread: if self._queue_management_thread:
# Wake up queue management thread
self._result_queue.put(None)
if wait:
self._queue_management_thread.join() self._queue_management_thread.join()
# To reduce the risk of openning too many files, remove references to # To reduce the risk of openning too many files, remove references to
# objects that use file descriptors. # objects that use file descriptors.
......
...@@ -25,28 +25,17 @@ import weakref ...@@ -25,28 +25,17 @@ import weakref
# workers to exit when their work queues are empty and then waits until the # workers to exit when their work queues are empty and then waits until the
# threads finish. # threads finish.
_thread_references = set() _threads_queues = weakref.WeakKeyDictionary()
_shutdown = False _shutdown = False
def _python_exit(): def _python_exit():
global _shutdown global _shutdown
_shutdown = True _shutdown = True
for thread_reference in _thread_references: items = list(_threads_queues.items())
thread = thread_reference() for t, q in items:
if thread is not None: q.put(None)
thread.join() for t, q in items:
t.join()
def _remove_dead_thread_references():
"""Remove inactive threads from _thread_references.
Should be called periodically to prevent memory leaks in scenarios such as:
>>> while True:
... t = ThreadPoolExecutor(max_workers=5)
... t.map(int, ['1', '2', '3', '4', '5'])
"""
for thread_reference in set(_thread_references):
if thread_reference() is None:
_thread_references.discard(thread_reference)
atexit.register(_python_exit) atexit.register(_python_exit)
...@@ -72,18 +61,23 @@ def _worker(executor_reference, work_queue): ...@@ -72,18 +61,23 @@ def _worker(executor_reference, work_queue):
try: try:
while True: while True:
try: try:
work_item = work_queue.get(block=True, timeout=0.1) work_item = work_queue.get(block=True)
except queue.Empty: except queue.Empty:
pass
else:
if work_item is not None:
work_item.run()
continue
executor = executor_reference() executor = executor_reference()
# Exit if: # Exit if:
# - The interpreter is shutting down OR # - The interpreter is shutting down OR
# - The executor that owns the worker has been collected OR # - The executor that owns the worker has been collected OR
# - The executor that owns the worker has been shutdown. # - The executor that owns the worker has been shutdown.
if _shutdown or executor is None or executor._shutdown: if _shutdown or executor is None or executor._shutdown:
# Notice other workers
work_queue.put(None)
return return
del executor del executor
else:
work_item.run()
except BaseException as e: except BaseException as e:
_base.LOGGER.critical('Exception in worker', exc_info=True) _base.LOGGER.critical('Exception in worker', exc_info=True)
...@@ -95,8 +89,6 @@ class ThreadPoolExecutor(_base.Executor): ...@@ -95,8 +89,6 @@ class ThreadPoolExecutor(_base.Executor):
max_workers: The maximum number of threads that can be used to max_workers: The maximum number of threads that can be used to
execute the given calls. execute the given calls.
""" """
_remove_dead_thread_references()
self._max_workers = max_workers self._max_workers = max_workers
self._work_queue = queue.Queue() self._work_queue = queue.Queue()
self._threads = set() self._threads = set()
...@@ -117,19 +109,25 @@ class ThreadPoolExecutor(_base.Executor): ...@@ -117,19 +109,25 @@ class ThreadPoolExecutor(_base.Executor):
submit.__doc__ = _base.Executor.submit.__doc__ submit.__doc__ = _base.Executor.submit.__doc__
def _adjust_thread_count(self): def _adjust_thread_count(self):
# When the executor gets lost, the weakref callback will wake up
# the worker threads.
def weakref_cb(_, q=self._work_queue):
q.put(None)
# TODO(bquinlan): Should avoid creating new threads if there are more # TODO(bquinlan): Should avoid creating new threads if there are more
# idle threads than items in the work queue. # idle threads than items in the work queue.
if len(self._threads) < self._max_workers: if len(self._threads) < self._max_workers:
t = threading.Thread(target=_worker, t = threading.Thread(target=_worker,
args=(weakref.ref(self), self._work_queue)) args=(weakref.ref(self, weakref_cb),
self._work_queue))
t.daemon = True t.daemon = True
t.start() t.start()
self._threads.add(t) self._threads.add(t)
_thread_references.add(weakref.ref(t)) _threads_queues[t] = self._work_queue
def shutdown(self, wait=True): def shutdown(self, wait=True):
with self._shutdown_lock: with self._shutdown_lock:
self._shutdown = True self._shutdown = True
self._work_queue.put(None)
if wait: if wait:
for t in self._threads: for t in self._threads:
t.join() t.join()
......
...@@ -53,6 +53,9 @@ Core and Builtins ...@@ -53,6 +53,9 @@ Core and Builtins
Library Library
------- -------
- Issue #11635: Don't use polling in worker threads and processes launched by
concurrent.futures.
- Issue #11628: cmp_to_key generated class should use __slots__ - Issue #11628: cmp_to_key generated class should use __slots__
- Issue #11666: let help() display named tuple attributes and methods - Issue #11666: let help() display named tuple attributes and methods
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment