Update futures to 3.2.0

This commit is contained in:
Labrys of Knossos 2018-12-16 10:24:14 -05:00
commit 49c9ea1350
5 changed files with 204 additions and 185 deletions

View file

@ -14,5 +14,10 @@ from concurrent.futures._base import (FIRST_COMPLETED,
Executor, Executor,
wait, wait,
as_completed) as_completed)
from concurrent.futures.process import ProcessPoolExecutor
from concurrent.futures.thread import ThreadPoolExecutor from concurrent.futures.thread import ThreadPoolExecutor
try:
from concurrent.futures.process import ProcessPoolExecutor
except ImportError:
# some platforms don't have multiprocessing
pass

View file

@ -1,15 +1,12 @@
# Copyright 2009 Brian Quinlan. All Rights Reserved. # Copyright 2009 Brian Quinlan. All Rights Reserved.
# Licensed to PSF under a Contributor Agreement. # Licensed to PSF under a Contributor Agreement.
from __future__ import with_statement import collections
import logging import logging
import threading import threading
import itertools
import time import time
import types
try:
from collections import namedtuple
except ImportError:
from concurrent.futures._compat import namedtuple
__author__ = 'Brian Quinlan (brian@sweetapp.com)' __author__ = 'Brian Quinlan (brian@sweetapp.com)'
@ -175,6 +172,29 @@ def _create_and_install_waiters(fs, return_when):
return waiter return waiter
def _yield_finished_futures(fs, waiter, ref_collect):
"""
Iterate on the list *fs*, yielding finished futures one by one in
reverse order.
Before yielding a future, *waiter* is removed from its waiters
and the future is removed from each set in the collection of sets
*ref_collect*.
The aim of this function is to avoid keeping stale references after
the future is yielded and before the iterator resumes.
"""
while fs:
f = fs[-1]
for futures_set in ref_collect:
futures_set.remove(f)
with f._condition:
f._waiters.remove(waiter)
del f
# Careful not to keep a reference to the popped value
yield fs.pop()
def as_completed(fs, timeout=None): def as_completed(fs, timeout=None):
"""An iterator over the given futures that yields each as it completes. """An iterator over the given futures that yields each as it completes.
@ -186,7 +206,8 @@ def as_completed(fs, timeout=None):
Returns: Returns:
An iterator that yields the given Futures as they complete (finished or An iterator that yields the given Futures as they complete (finished or
cancelled). cancelled). If any given Futures are duplicated, they will be returned
once.
Raises: Raises:
TimeoutError: If the entire result iterator could not be generated TimeoutError: If the entire result iterator could not be generated
@ -195,16 +216,20 @@ def as_completed(fs, timeout=None):
if timeout is not None: if timeout is not None:
end_time = timeout + time.time() end_time = timeout + time.time()
fs = set(fs)
total_futures = len(fs)
with _AcquireFutures(fs): with _AcquireFutures(fs):
finished = set( finished = set(
f for f in fs f for f in fs
if f._state in [CANCELLED_AND_NOTIFIED, FINISHED]) if f._state in [CANCELLED_AND_NOTIFIED, FINISHED])
pending = set(fs) - finished pending = fs - finished
waiter = _create_and_install_waiters(fs, _AS_COMPLETED) waiter = _create_and_install_waiters(fs, _AS_COMPLETED)
finished = list(finished)
try: try:
for future in finished: for f in _yield_finished_futures(finished, waiter,
yield future ref_collect=(fs,)):
f = [f]
yield f.pop()
while pending: while pending:
if timeout is None: if timeout is None:
@ -214,7 +239,7 @@ def as_completed(fs, timeout=None):
if wait_timeout < 0: if wait_timeout < 0:
raise TimeoutError( raise TimeoutError(
'%d (of %d) futures unfinished' % ( '%d (of %d) futures unfinished' % (
len(pending), len(fs))) len(pending), total_futures))
waiter.event.wait(wait_timeout) waiter.event.wait(wait_timeout)
@ -223,15 +248,20 @@ def as_completed(fs, timeout=None):
waiter.finished_futures = [] waiter.finished_futures = []
waiter.event.clear() waiter.event.clear()
for future in finished: # reverse to keep finishing order
yield future finished.reverse()
pending.remove(future) for f in _yield_finished_futures(finished, waiter,
ref_collect=(fs, pending)):
f = [f]
yield f.pop()
finally: finally:
# Remove waiter from unfinished futures
for f in fs: for f in fs:
f._waiters.remove(waiter) with f._condition:
f._waiters.remove(waiter)
DoneAndNotDoneFutures = namedtuple( DoneAndNotDoneFutures = collections.namedtuple(
'DoneAndNotDoneFutures', 'done not_done') 'DoneAndNotDoneFutures', 'done not_done')
def wait(fs, timeout=None, return_when=ALL_COMPLETED): def wait(fs, timeout=None, return_when=ALL_COMPLETED):
"""Wait for the futures in the given sequence to complete. """Wait for the futures in the given sequence to complete.
@ -276,7 +306,8 @@ def wait(fs, timeout=None, return_when=ALL_COMPLETED):
waiter.event.wait(timeout) waiter.event.wait(timeout)
for f in fs: for f in fs:
f._waiters.remove(waiter) with f._condition:
f._waiters.remove(waiter)
done.update(waiter.finished_futures) done.update(waiter.finished_futures)
return DoneAndNotDoneFutures(done, set(fs) - done) return DoneAndNotDoneFutures(done, set(fs) - done)
@ -290,6 +321,7 @@ class Future(object):
self._state = PENDING self._state = PENDING
self._result = None self._result = None
self._exception = None self._exception = None
self._traceback = None
self._waiters = [] self._waiters = []
self._done_callbacks = [] self._done_callbacks = []
@ -299,22 +331,41 @@ class Future(object):
callback(self) callback(self)
except Exception: except Exception:
LOGGER.exception('exception calling callback for %r', self) LOGGER.exception('exception calling callback for %r', self)
except BaseException:
# Explicitly let all other new-style exceptions through so
# that we can catch all old-style exceptions with a simple
# "except:" clause below.
#
# All old-style exception objects are instances of
# types.InstanceType, but "except types.InstanceType:" does
# not catch old-style exceptions for some reason. Thus, the
# only way to catch all old-style exceptions without catching
# any new-style exceptions is to filter out the new-style
# exceptions, which all derive from BaseException.
raise
except:
# Because of the BaseException clause above, this handler only
# executes for old-style exception objects.
LOGGER.exception('exception calling callback for %r', self)
def __repr__(self): def __repr__(self):
with self._condition: with self._condition:
if self._state == FINISHED: if self._state == FINISHED:
if self._exception: if self._exception:
return '<Future at %s state=%s raised %s>' % ( return '<%s at %#x state=%s raised %s>' % (
hex(id(self)), self.__class__.__name__,
id(self),
_STATE_TO_DESCRIPTION_MAP[self._state], _STATE_TO_DESCRIPTION_MAP[self._state],
self._exception.__class__.__name__) self._exception.__class__.__name__)
else: else:
return '<Future at %s state=%s returned %s>' % ( return '<%s at %#x state=%s returned %s>' % (
hex(id(self)), self.__class__.__name__,
id(self),
_STATE_TO_DESCRIPTION_MAP[self._state], _STATE_TO_DESCRIPTION_MAP[self._state],
self._result.__class__.__name__) self._result.__class__.__name__)
return '<Future at %s state=%s>' % ( return '<%s at %#x state=%s>' % (
hex(id(self)), self.__class__.__name__,
id(self),
_STATE_TO_DESCRIPTION_MAP[self._state]) _STATE_TO_DESCRIPTION_MAP[self._state])
def cancel(self): def cancel(self):
@ -337,7 +388,7 @@ class Future(object):
return True return True
def cancelled(self): def cancelled(self):
"""Return True if the future has cancelled.""" """Return True if the future was cancelled."""
with self._condition: with self._condition:
return self._state in [CANCELLED, CANCELLED_AND_NOTIFIED] return self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]
@ -353,7 +404,14 @@ class Future(object):
def __get_result(self): def __get_result(self):
if self._exception: if self._exception:
raise self._exception if isinstance(self._exception, types.InstanceType):
# The exception is an instance of an old-style class, which
# means type(self._exception) returns types.ClassType instead
# of the exception's actual class type.
exception_type = self._exception.__class__
else:
exception_type = type(self._exception)
raise exception_type, self._exception, self._traceback
else: else:
return self._result return self._result
@ -405,6 +463,39 @@ class Future(object):
else: else:
raise TimeoutError() raise TimeoutError()
def exception_info(self, timeout=None):
"""Return a tuple of (exception, traceback) raised by the call that the
future represents.
Args:
timeout: The number of seconds to wait for the exception if the
future isn't done. If None, then there is no limit on the wait
time.
Returns:
The exception raised by the call that the future represents or None
if the call completed without raising.
Raises:
CancelledError: If the future was cancelled.
TimeoutError: If the future didn't finish executing before the given
timeout.
"""
with self._condition:
if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:
raise CancelledError()
elif self._state == FINISHED:
return self._exception, self._traceback
self._condition.wait(timeout)
if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:
raise CancelledError()
elif self._state == FINISHED:
return self._exception, self._traceback
else:
raise TimeoutError()
def exception(self, timeout=None): def exception(self, timeout=None):
"""Return the exception raised by the call that the future represents. """Return the exception raised by the call that the future represents.
@ -422,21 +513,7 @@ class Future(object):
TimeoutError: If the future didn't finish executing before the given TimeoutError: If the future didn't finish executing before the given
timeout. timeout.
""" """
return self.exception_info(timeout)[0]
with self._condition:
if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:
raise CancelledError()
elif self._state == FINISHED:
return self._exception
self._condition.wait(timeout)
if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:
raise CancelledError()
elif self._state == FINISHED:
return self._exception
else:
raise TimeoutError()
# The following methods should only be used by Executors and in tests. # The following methods should only be used by Executors and in tests.
def set_running_or_notify_cancel(self): def set_running_or_notify_cancel(self):
@ -475,8 +552,8 @@ class Future(object):
return True return True
else: else:
LOGGER.critical('Future %s in unexpected state: %s', LOGGER.critical('Future %s in unexpected state: %s',
id(self.future), id(self),
self.future._state) self._state)
raise RuntimeError('Future in unexpected state') raise RuntimeError('Future in unexpected state')
def set_result(self, result): def set_result(self, result):
@ -492,19 +569,28 @@ class Future(object):
self._condition.notify_all() self._condition.notify_all()
self._invoke_callbacks() self._invoke_callbacks()
def set_exception(self, exception): def set_exception_info(self, exception, traceback):
"""Sets the result of the future as being the given exception. """Sets the result of the future as being the given exception
and traceback.
Should only be used by Executor implementations and unit tests. Should only be used by Executor implementations and unit tests.
""" """
with self._condition: with self._condition:
self._exception = exception self._exception = exception
self._traceback = traceback
self._state = FINISHED self._state = FINISHED
for waiter in self._waiters: for waiter in self._waiters:
waiter.add_exception(self) waiter.add_exception(self)
self._condition.notify_all() self._condition.notify_all()
self._invoke_callbacks() self._invoke_callbacks()
def set_exception(self, exception):
"""Sets the result of the future as being the given exception.
Should only be used by Executor implementations and unit tests.
"""
self.set_exception_info(exception, None)
class Executor(object): class Executor(object):
"""This is an abstract base class for concrete asynchronous executors.""" """This is an abstract base class for concrete asynchronous executors."""
@ -520,7 +606,7 @@ class Executor(object):
raise NotImplementedError() raise NotImplementedError()
def map(self, fn, *iterables, **kwargs): def map(self, fn, *iterables, **kwargs):
"""Returns a iterator equivalent to map(fn, iter). """Returns an iterator equivalent to map(fn, iter).
Args: Args:
fn: A callable that will take as many arguments as there are fn: A callable that will take as many arguments as there are
@ -541,17 +627,24 @@ class Executor(object):
if timeout is not None: if timeout is not None:
end_time = timeout + time.time() end_time = timeout + time.time()
fs = [self.submit(fn, *args) for args in zip(*iterables)] fs = [self.submit(fn, *args) for args in itertools.izip(*iterables)]
try: # Yield must be hidden in closure so that the futures are submitted
for future in fs: # before the first iterator value is required.
if timeout is None: def result_iterator():
yield future.result() try:
else: # reverse to keep finishing order
yield future.result(end_time - time.time()) fs.reverse()
finally: while fs:
for future in fs: # Careful not to keep a reference to the popped future
future.cancel() if timeout is None:
yield fs.pop().result()
else:
yield fs.pop().result(end_time - time.time())
finally:
for future in fs:
future.cancel()
return result_iterator()
def shutdown(self, wait=True): def shutdown(self, wait=True):
"""Clean-up the resources associated with the Executor. """Clean-up the resources associated with the Executor.

View file

@ -1,101 +0,0 @@
from keyword import iskeyword as _iskeyword
from operator import itemgetter as _itemgetter
import sys as _sys
def namedtuple(typename, field_names):
"""Returns a new subclass of tuple with named fields.
>>> Point = namedtuple('Point', 'x y')
>>> Point.__doc__ # docstring for the new class
'Point(x, y)'
>>> p = Point(11, y=22) # instantiate with positional args or keywords
>>> p[0] + p[1] # indexable like a plain tuple
33
>>> x, y = p # unpack like a regular tuple
>>> x, y
(11, 22)
>>> p.x + p.y # fields also accessable by name
33
>>> d = p._asdict() # convert to a dictionary
>>> d['x']
11
>>> Point(**d) # convert from a dictionary
Point(x=11, y=22)
>>> p._replace(x=100) # _replace() is like str.replace() but targets named fields
Point(x=100, y=22)
"""
# Parse and validate the field names. Validation serves two purposes,
# generating informative error messages and preventing template injection attacks.
if isinstance(field_names, basestring):
field_names = field_names.replace(',', ' ').split() # names separated by whitespace and/or commas
field_names = tuple(map(str, field_names))
for name in (typename,) + field_names:
if not all(c.isalnum() or c=='_' for c in name):
raise ValueError('Type names and field names can only contain alphanumeric characters and underscores: %r' % name)
if _iskeyword(name):
raise ValueError('Type names and field names cannot be a keyword: %r' % name)
if name[0].isdigit():
raise ValueError('Type names and field names cannot start with a number: %r' % name)
seen_names = set()
for name in field_names:
if name.startswith('_'):
raise ValueError('Field names cannot start with an underscore: %r' % name)
if name in seen_names:
raise ValueError('Encountered duplicate field name: %r' % name)
seen_names.add(name)
# Create and fill-in the class template
numfields = len(field_names)
argtxt = repr(field_names).replace("'", "")[1:-1] # tuple repr without parens or quotes
reprtxt = ', '.join('%s=%%r' % name for name in field_names)
dicttxt = ', '.join('%r: t[%d]' % (name, pos) for pos, name in enumerate(field_names))
template = '''class %(typename)s(tuple):
'%(typename)s(%(argtxt)s)' \n
__slots__ = () \n
_fields = %(field_names)r \n
def __new__(_cls, %(argtxt)s):
return _tuple.__new__(_cls, (%(argtxt)s)) \n
@classmethod
def _make(cls, iterable, new=tuple.__new__, len=len):
'Make a new %(typename)s object from a sequence or iterable'
result = new(cls, iterable)
if len(result) != %(numfields)d:
raise TypeError('Expected %(numfields)d arguments, got %%d' %% len(result))
return result \n
def __repr__(self):
return '%(typename)s(%(reprtxt)s)' %% self \n
def _asdict(t):
'Return a new dict which maps field names to their values'
return {%(dicttxt)s} \n
def _replace(_self, **kwds):
'Return a new %(typename)s object replacing specified fields with new values'
result = _self._make(map(kwds.pop, %(field_names)r, _self))
if kwds:
raise ValueError('Got unexpected field names: %%r' %% kwds.keys())
return result \n
def __getnewargs__(self):
return tuple(self) \n\n''' % locals()
for i, name in enumerate(field_names):
template += ' %s = _property(_itemgetter(%d))\n' % (name, i)
# Execute the template string in a temporary namespace and
# support tracing utilities by setting a value for frame.f_globals['__name__']
namespace = dict(_itemgetter=_itemgetter, __name__='namedtuple_%s' % typename,
_property=property, _tuple=tuple)
try:
exec(template, namespace)
except SyntaxError:
e = _sys.exc_info()[1]
raise SyntaxError(e.message + ':\n' + template)
result = namespace[typename]
# For pickling to work, the __module__ variable needs to be set to the frame
# where the named tuple is created. Bypass this step in enviroments where
# sys._getframe is not defined (Jython for example).
if hasattr(_sys, '_getframe'):
result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__')
return result

View file

@ -43,20 +43,14 @@ Process #1..n:
_ResultItems in "Request Q" _ResultItems in "Request Q"
""" """
from __future__ import with_statement
import atexit import atexit
from concurrent.futures import _base
import Queue as queue
import multiprocessing import multiprocessing
import threading import threading
import weakref import weakref
import sys import sys
from concurrent.futures import _base
try:
import queue
except ImportError:
import Queue as queue
__author__ = 'Brian Quinlan (brian@sweetapp.com)' __author__ = 'Brian Quinlan (brian@sweetapp.com)'
# Workers are created as daemon threads and processes. This is done to allow the # Workers are created as daemon threads and processes. This is done to allow the
@ -79,11 +73,11 @@ _shutdown = False
def _python_exit(): def _python_exit():
global _shutdown global _shutdown
_shutdown = True _shutdown = True
items = list(_threads_queues.items()) items = list(_threads_queues.items()) if _threads_queues else ()
for t, q in items: for t, q in items:
q.put(None) q.put(None)
for t, q in items: for t, q in items:
t.join() t.join(sys.maxint)
# 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
@ -132,7 +126,7 @@ def _process_worker(call_queue, result_queue):
return 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: except:
e = sys.exc_info()[1] e = sys.exc_info()[1]
result_queue.put(_ResultItem(call_item.work_id, result_queue.put(_ResultItem(call_item.work_id,
exception=e)) exception=e))
@ -220,6 +214,8 @@ def _queue_management_worker(executor_reference,
work_item.future.set_exception(result_item.exception) work_item.future.set_exception(result_item.exception)
else: else:
work_item.future.set_result(result_item.result) work_item.future.set_result(result_item.result)
# Delete references to object. See issue16284
del work_item
# Check whether we should start shutting down. # 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:
@ -266,6 +262,7 @@ def _check_system_limits():
_system_limited = "system provides too few semaphores (%d available, 256 necessary)" % nsems_max _system_limited = "system provides too few semaphores (%d available, 256 necessary)" % nsems_max
raise NotImplementedError(_system_limited) raise NotImplementedError(_system_limited)
class ProcessPoolExecutor(_base.Executor): class ProcessPoolExecutor(_base.Executor):
def __init__(self, max_workers=None): def __init__(self, max_workers=None):
"""Initializes a new ProcessPoolExecutor instance. """Initializes a new ProcessPoolExecutor instance.
@ -280,6 +277,9 @@ class ProcessPoolExecutor(_base.Executor):
if max_workers is None: if max_workers is None:
self._max_workers = multiprocessing.cpu_count() self._max_workers = multiprocessing.cpu_count()
else: else:
if max_workers <= 0:
raise ValueError("max_workers must be greater than 0")
self._max_workers = max_workers self._max_workers = max_workers
# Make the call queue slightly larger than the number of processes to # Make the call queue slightly larger than the number of processes to
@ -351,7 +351,7 @@ class ProcessPoolExecutor(_base.Executor):
# Wake up queue management thread # Wake up queue management thread
self._result_queue.put(None) self._result_queue.put(None)
if wait: if wait:
self._queue_management_thread.join() self._queue_management_thread.join(sys.maxint)
# 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.
self._queue_management_thread = None self._queue_management_thread = None

View file

@ -3,18 +3,20 @@
"""Implements ThreadPoolExecutor.""" """Implements ThreadPoolExecutor."""
from __future__ import with_statement
import atexit import atexit
from concurrent.futures import _base
import itertools
import Queue as queue
import threading import threading
import weakref import weakref
import sys import sys
from concurrent.futures import _base
try: try:
import queue from multiprocessing import cpu_count
except ImportError: except ImportError:
import Queue as queue # some platforms don't have multiprocessing
def cpu_count():
return None
__author__ = 'Brian Quinlan (brian@sweetapp.com)' __author__ = 'Brian Quinlan (brian@sweetapp.com)'
@ -38,11 +40,11 @@ _shutdown = False
def _python_exit(): def _python_exit():
global _shutdown global _shutdown
_shutdown = True _shutdown = True
items = list(_threads_queues.items()) items = list(_threads_queues.items()) if _threads_queues else ()
for t, q in items: for t, q in items:
q.put(None) q.put(None)
for t, q in items: for t, q in items:
t.join() t.join(sys.maxint)
atexit.register(_python_exit) atexit.register(_python_exit)
@ -59,9 +61,9 @@ class _WorkItem(object):
try: try:
result = self.fn(*self.args, **self.kwargs) result = self.fn(*self.args, **self.kwargs)
except BaseException: except:
e = sys.exc_info()[1] e, tb = sys.exc_info()[1:]
self.future.set_exception(e) self.future.set_exception_info(e, tb)
else: else:
self.future.set_result(result) self.future.set_result(result)
@ -71,6 +73,8 @@ def _worker(executor_reference, work_queue):
work_item = work_queue.get(block=True) work_item = work_queue.get(block=True)
if work_item is not None: if work_item is not None:
work_item.run() work_item.run()
# Delete references to object. See issue16284
del work_item
continue continue
executor = executor_reference() executor = executor_reference()
# Exit if: # Exit if:
@ -82,22 +86,37 @@ def _worker(executor_reference, work_queue):
work_queue.put(None) work_queue.put(None)
return return
del executor del executor
except BaseException: except:
_base.LOGGER.critical('Exception in worker', exc_info=True) _base.LOGGER.critical('Exception in worker', exc_info=True)
class ThreadPoolExecutor(_base.Executor): class ThreadPoolExecutor(_base.Executor):
def __init__(self, max_workers):
# Used to assign unique thread names when thread_name_prefix is not supplied.
_counter = itertools.count().next
def __init__(self, max_workers=None, thread_name_prefix=''):
"""Initializes a new ThreadPoolExecutor instance. """Initializes a new ThreadPoolExecutor instance.
Args: Args:
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.
thread_name_prefix: An optional name prefix to give our threads.
""" """
if max_workers is None:
# Use this number because ThreadPoolExecutor is often
# used to overlap I/O instead of CPU work.
max_workers = (cpu_count() or 1) * 5
if max_workers <= 0:
raise ValueError("max_workers must be greater than 0")
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()
self._shutdown = False self._shutdown = False
self._shutdown_lock = threading.Lock() self._shutdown_lock = threading.Lock()
self._thread_name_prefix = (thread_name_prefix or
("ThreadPoolExecutor-%d" % self._counter()))
def submit(self, fn, *args, **kwargs): def submit(self, fn, *args, **kwargs):
with self._shutdown_lock: with self._shutdown_lock:
@ -119,8 +138,11 @@ class ThreadPoolExecutor(_base.Executor):
q.put(None) 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: num_threads = len(self._threads)
t = threading.Thread(target=_worker, 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,
args=(weakref.ref(self, weakref_cb), args=(weakref.ref(self, weakref_cb),
self._work_queue)) self._work_queue))
t.daemon = True t.daemon = True
@ -134,5 +156,5 @@ class ThreadPoolExecutor(_base.Executor):
self._work_queue.put(None) self._work_queue.put(None)
if wait: if wait:
for t in self._threads: for t in self._threads:
t.join() t.join(sys.maxint)
shutdown.__doc__ = _base.Executor.shutdown.__doc__ shutdown.__doc__ = _base.Executor.shutdown.__doc__