标准库 concurrent.futures 对多线程编程中的线程池与期物进行了高阶封装,并且在多线程、多进程编程中, futures提供了高度一致的接口,因此学习 ThreadPoolExecutor 与 Future 不仅对 Python 多线程编程有很大的帮助,对 Python 多进程编程也有帮助。
Future
Future 一般称作“期物”,可以理解为一个容器对象,该容器里封装了待执行的异步计算。期物一般不是由开发者主动生成,而是通过调用线程池执行器的 submit 方法, 由线程池执行器自动生成期物。 其的初始化源码如下:
class Future(object):
"""Represents the result of an asynchronous computation."""
def __init__(self):
"""Initializes the future. Should not be called by clients."""
self._condition = threading.Condition() # 每一期物内部包含一个条件变量,条件变量内部默认使用可重入锁RLock
self._state = PENDING # 新创建的期物默认为“挂起”状态, 常见的期物的状态还包括运行、取消、完成三种状态
self._result = None # 存储异步执行的结果
self._exception = None # 存储异步执行中的异常
self._waiters = [] # 用于期物对象获取计算结果的过程,用于 as_complete() 或者 wait() 方法
self._done_callbacks = [] # 如果指定了回调函数,存储在这里
调用期物的 result 方法,可以获取期物封装的异步执行的结果:
def result(self, timeout=None):
try:
with self._condition:
if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:
raise CancelledError()
elif self._state == FINISHED:
return self.__get_result()
self._condition.wait(timeout) # 如果期物没有完成、也没有被取消,则等待异步执行的结果
if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:
raise CancelledError()
elif self._state == FINISHED:
return self.__get_result() # __get_result() 方法会返回 self._result 或者抛出 self._exception
else:
raise TimeoutError()
finally:
# Break a reference cycle with the exception in self._exception
self = None
在上述代码中,并未看到期物与待执行函数的关系,也没有看到函数运行的结果或者异常是如何存储到期物的 self._result 或者 self._exception 属性中的, 实际上期物是通过 set_result 与 set_exception 将执行结果与异常分别写到期物对应的属性中:
def set_result(self, result):
"""Sets the return value of work associated with the future.
Should only be used by Executor implementations and unit tests.
"""
with self._condition:
if self._state in {CANCELLED, CANCELLED_AND_NOTIFIED, FINISHED}:
raise InvalidStateError('{}: {!r}'.format(self._state, self))
self._result = result # 将结果写到 _result 属性
self._state = FINISHED # 设置状态为完成
for waiter in self._waiters:
waiter.add_result(self) # waiter 对象内部有个属性叫 finished_futures, 状态完成的期物会加入到该列表,并调用event.set() 唤醒被阻塞的线程,在 as_complete() 中会创建一个 waiter对象, 并将该 waiter 对象添加到所有的期物对象_waiter 属性中,则可以通过同一 waiter 对象来查看一个期物序列的完成状态
self._condition.notify_all() # 在期物的 result 方法中调用了 wait 方法,这里调用 notify_all 可以唤醒阻塞线程
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.
"""
with self._condition:
if self._state in {CANCELLED, CANCELLED_AND_NOTIFIED, FINISHED}:
raise InvalidStateError('{}: {!r}'.format(self._state, self))
self._exception = exception
self._state = FINISHED
for waiter in self._waiters:
waiter.add_exception(self)
self._condition.notify_all()
self._invoke_callbacks()
set_result 与 set_exception 的逻辑基本一致,不过一个是处理正常返回、另一个是处理异常。在代码中需要十分注意 waiter 对象, waiter 对象是什么, 又是什么时候插入到期物的 self._waiters 列表中的呢?实际上需要 waiter 对象是在 as_complete() 或者 wait() 方法中创建的,但需要理解 waiter 对象的作用, 需要先了解线程池执行器、以及线程池执行器与期物的关系。
ThreadPoolExecutor
使用线程池执行器对线程数量、执行结果、执行异常等进行管理都十分方便,其初始化源码如下:
class ThreadPoolExecutor(_base.Executor):
# Used to assign unique thread names when thread_name_prefix is not supplied.
_counter = itertools.count().__next__ # 是一个方法,依次返回0、1、2、3......
def __init__(self, max_workers=None, thread_name_prefix='',
initializer=None, initargs=()):
if max_workers is None:
max_workers = min(32, (os.cpu_count() or 1) + 4)
if max_workers <= 0:
raise ValueError("max_workers must be greater than 0")
if initializer is not None and not callable(initializer):
raise TypeError("initializer must be a callable")
self._max_workers = max_workers
self._work_queue = queue.SimpleQueue() # 用于存储_WorkItem对象, _WorkItem对象中封装了期物,待执行的函数及函数需要的参数
self._idle_semaphore = threading.Semaphore(0)
self._threads = set()
self._broken = False
self._shutdown = False # 存储进程池的状态
self._shutdown_lock = threading.Lock() # 控制进程池的状态
self._thread_name_prefix = (thread_name_prefix or
("ThreadPoolExecutor-%d" % self._counter()))
self._initializer = initializer # 对每一线程进行初始化
self._initargs = initargs # 初始化所需的参数
在初始化源码中,需要重点关注线程池执行器的 _work_queue 属性,该队列中存储的就是一个个待计算的单元。通过线程池执行器的 submit 方法生成期物、_WorkItem 对象:
def submit(*args, **kwargs):
"""Submits a callable to be executed with the given arguments.
Schedules the callable to be executed as fn(*args, **kwargs) and returns
a Future instance representing the execution of the callable.
Returns:
A Future representing the given call.
"""
if len(args) >= 2:
self, fn, *args = args
elif not args:
raise TypeError("descriptor 'submit' of 'ThreadPoolExecutor' object "
"needs an argument")
elif 'fn' in kwargs:
fn = kwargs.pop('fn')
self, *args = args
import warnings
warnings.warn("Passing 'fn' as keyword argument is deprecated",
DeprecationWarning, stacklevel=2)
else:
raise TypeError('submit expected at least 1 positional argument, '
'got %d' % (len(args)-1))
with self._shutdown_lock:
if self._broken:
raise BrokenThreadPool(self._broken)
if self._shutdown:
raise RuntimeError('cannot schedule new futures after shutdown')
if _shutdown:
raise RuntimeError('cannot schedule new futures after '
'interpreter shutdown')
f = _base.Future() # 创建一个期物
w = _WorkItem(f, fn, args, kwargs) # 将期物与待执行的函数、参数封装成一个基本工作单元
self._work_queue.put(w)
self._adjust_thread_count() # 调整线程的计数
return f
在 submit 中首先创建了期物与 _WorkItem 对象, 但似乎并没有发现 threading.Thread 的影子, 实际上创建线程与启动线程发生在 self._adjust_thread_count() 方法, 在此此前先看下 _WorkItem 对象的定义, 关键是 run 方法:
class _WorkItem(object):
def __init__(self, future, fn, args, kwargs):
self.future = future
self.fn = fn
self.args = args
self.kwargs = kwargs
def run(self):
if not self.future.set_running_or_notify_cancel():
return
try:
result = self.fn(*self.args, **self.kwargs) # 执行函数
except BaseException as exc:
self.future.set_exception(exc) # 将异常写入期物的_exception 属性
# Break a reference cycle with the exception 'exc'
self = None
else:
self.future.set_result(result) # 将异常写入期物的_result 属性
注意 _WorkItem 类的定义与继承 threading.Thread 的方式非常相似,但是 _WorkItem 并非是一个子线程类。
def _adjust_thread_count(self):
# if idle threads are available, don't spin new threads
if self._idle_semaphore.acquire(timeout=0):
return
# When the executor gets lost, the weakref callback will wake up
# the worker threads.
def weakref_cb(_, q=self._work_queue):
q.put(None)
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,
args=(weakref.ref(self, weakref_cb),
self._work_queue,
self._initializer,
self._initargs)) # 创建线程
t.daemon = True
t.start() # 启动线程
self._threads.add(t)
_threads_queues[t] = self._work_queue
可以看到 _adjust_thread_count 方法不像名称上所示,仅调整线程的计数,实际上该函数不仅调整线程计数(线程计数是为了确保线程名称的唯一),而且创建与启动了线程,并且还会将执行结果设置到期物中,因此 _adjust_thread_count 函数十分重要。另外注意在创建线程时,target 方法接受到的是 _worker,_worker 与 _WorkItem 有什么关系呢?来看下 _worker 函数:
def _worker(executor_reference, work_queue, initializer, initargs):
if initializer is not None:
try:
initializer(*initargs)
except BaseException:
_base.LOGGER.critical('Exception in initializer:', exc_info=True)
executor = executor_reference()
if executor is not None:
executor._initializer_failed()
return
try:
while True:
work_item = work_queue.get(block=True) # work_queue 就是线程池的 work_queue 属性,会从 work_queue 队列中不断取出work_item 对象
if work_item is not None: # work_queue 队列存在 None 对象
work_item.run() # 执行函数计算,并将执行结果写入期物对象
# Delete references to object. See issue16284
del work_item
# attempt to increment idle count
executor = executor_reference() # TODO: 弱引用原理
if executor is not None:
executor._idle_semaphore.release()
del executor
continue
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:
# 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
# Notice other workers
work_queue.put(None)
return
del executor
except BaseException:
_base.LOGGER.critical('Exception in worker', exc_info=True)
_worker 函数函数中需要重点关注 work_queue 参数, 实际上该参数接受到的实参就是线程池执行器的 _work_queue 队列,线程池执行器中的 submit 函数相当于是个生产者函数,submit 函数不停的创建 _WorkItem 对象, 而 _worker 函数相当于消费者函数,不停的消耗 _WorkItem 对象。另外有一点需要注意, 生产者只有一个,而消费者的数量最多可以有 max_workers 个,每一个消费者都是线程池执行器中的一个线程。
as_complete() 与 wait()
了解了期物、线程池执行器及其关系,还有一个问题未解决,就是前文提到的期物中 waiter 对象的作用是什么?可以先考虑一个问题,调用线程池执行器中的 submit 方法提交计算任务, 线程池中的线程(如果有空闲线程会直接使用空闲线程,如果没有空闲线程并且线程总数量小于 max_worker,会创建新线程),会启用 _worker 函数执行计算任务,那么怎样获取结算结果? 答案比较明显,肯定是通过期物对象——在期物部分已经介绍了期物的 set_result 与 set_exception 方法。 那么究竟是在什么时候能拿到计算结果,或者说什么时候将计算结果赋值给期物的呢?
要解决什么时候能拿到计算结果的问题,首先梳理进程池中的线程的执行逻辑,线程池启动后,会不停的从线程池执行器的 _work_queue 队列中获取 _WorkItem 来消费,消费 _WorkItem 会启动 _WorkItem 的 run 方法,run 方法会先执行线程池执行器 submit 方法中提交的函数, 然后将执行结果写入期物对象,关注期物的 set_result 方法:
def set_result(self, result):
# ......
for waiter in self._waiters:
waiter.add_result(self)
# ......
set_result 方法会从期物的 _waiters 属性列表中依次调用 waiter 对象的 add_result 方法, 到这里就需要弄清楚 waiter 对象到底是什么,以及有什么功能?在 futures 中定义了 _Waiter、 _AsCompletedWaiter、 _FirstCompletedWaiter、 _AllCompletedWaiter,第一个类是后三者的父类, 后三者针对“获取一个期物序列的所有结果”在不同策略下定义的,以 _Waiter、 _AsCompletedWaiter 为例:
class _AsCompletedWaiter(_Waiter):
"""Used by as_completed()."""
def __init__(self):
super(_AsCompletedWaiter, self).__init__()
self.lock = threading.Lock()
def add_result(self, future):
with self.lock:
super(_AsCompletedWaiter, self).add_result(future)
self.event.set() # 唤醒被阻塞的线程
def add_exception(self, future):
with self.lock:
super(_AsCompletedWaiter, self).add_exception(future)
self.event.set()
def add_cancelled(self, future):
with self.lock:
super(_AsCompletedWaiter, self).add_cancelled(future)
self.event.set()
# _Waiter 是各种不同 waiter 对象的基础类
class _Waiter(object):
"""Provides the event that wait() and as_completed() block on."""
def __init__(self):
self.event = threading.Event()
self.finished_futures = []
def add_result(self, future):
self.finished_futures.append(future)
def add_exception(self, future):
self.finished_futures.append(future)
def add_cancelled(self, future):
self.finished_futures.append(future)
从 _AsCompletedWaiter 的注释中可以看到, _AsCompletedWaiter 用于 as_complete 函数, 说明 waiter 对象是在 as_complete 函数中创建的。在查看 as_complete 函数源码之前, 先看下期物中 set_result 方法中 waiter.add_result(self)
, 对于 _AsCompletedWaiter对象,实际上, 会将 future 对象添加到 waiter 对象的 finished_futures,并且调用 waiter 对象的 event 属性的 set 方法, 这番操作的含义是表明该期物对象已经是完成状态了。 再来看 waiter 对象在 as_complete 函数中创建的位置与使用方式:
def as_completed(fs, timeout=None):
"""An iterator over the given futures that yields each as it completes.
Args:
fs: The sequence of Futures (possibly created by different Executors) to
iterate over.
timeout: The maximum number of seconds to wait. If None, then there
is no limit on the wait time.
Returns:
An iterator that yields the given Futures as they complete (finished or
cancelled). If any given Futures are duplicated, they will be returned
once.
Raises:
TimeoutError: If the entire result iterator could not be generated
before the given timeout.
"""
if timeout is not None:
end_time = timeout + time.monotonic()
fs = set(fs)
total_futures = len(fs)
with _AcquireFutures(fs): # 对期物序列进行排序
finished = set(
f for f in fs
if f._state in [CANCELLED_AND_NOTIFIED, FINISHED])
pending = fs - finished
waiter = _create_and_install_waiters(fs, _AS_COMPLETED) # ** 在这创建 waiter **
finished = list(finished)
try:
yield from _yield_finished_futures(finished, waiter,
ref_collect=(fs,)) # 生成器语法,将状态为已完成的期物依次输出
while pending: # 状态为pending的期物集合
if timeout is None:
wait_timeout = None
else:
wait_timeout = end_time - time.monotonic()
if wait_timeout < 0:
raise TimeoutError(
'%d (of %d) futures unfinished' % (
len(pending), total_futures))
waiter.event.wait(wait_timeout) # 等待计算结果,会被event.set() 方法唤醒
with waiter.lock:
finished = waiter.finished_futures
waiter.finished_futures = []
waiter.event.clear()
# reverse to keep finishing order
finished.reverse()
yield from _yield_finished_futures(finished, waiter,
ref_collect=(fs, pending))
finally:
# Remove waiter from unfinished futures
for f in fs:
with f._condition:
f._waiters.remove(waiter)
_create_and_install_waiters 函数创建 waiter, 查看其源码:
def _create_and_install_waiters(fs, return_when):
# return_when 有四种状态,_AS_COMPLETED 用于 as_complete 函数, 还有 FIRST_COMPLETED、FIRST_EXCEPTION、ALL_COMPLETED, 另外三者用于 wait 函数。
if return_when == _AS_COMPLETED:
waiter = _AsCompletedWaiter()
elif return_when == FIRST_COMPLETED:
waiter = _FirstCompletedWaiter()
else:
pending_count = sum(
f._state not in [CANCELLED_AND_NOTIFIED, FINISHED] for f in fs)
if return_when == FIRST_EXCEPTION:
waiter = _AllCompletedWaiter(pending_count, stop_on_exception=True)
elif return_when == ALL_COMPLETED:
waiter = _AllCompletedWaiter(pending_count, stop_on_exception=False)
else:
raise ValueError("Invalid return condition: %r" % return_when)
for f in fs:
f._waiters.append(waiter) # 对每一个期物将新创建的 waiter 对象加入期物的 _waiters 属性
return waiter
在 _create_and_install_waiters 函数中, 最重要的地方是 for f in fs: f._waiters.append(waiter)
,这个操作是将“同一 waiter 对象” 添加到所有期物中, 这样可以使用同一 waiter 对象来管理所有的期物对象。 另外在 as_complete 方法中还需关注两个地方, 其一是使用了 “generate from” 生成器语法, 使得 as_complete 返回一个生成器,并以此输出已完成状态的期物;其二关注生成器输出的每一元素做了哪些处理,这个逻辑存储在 _yield_finished_futures 函数中, 主要是处理引用关系:
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) # 已经完成状态的期物剔除 waiter 对象
del f
# Careful not to keep a reference to the popped value
yield fs.pop()
至此就完成了期物、线程池执行器、as_complete 方法之间的逻辑关系梳理。另外再补充一个 wait 方法的说明:
def wait(fs, timeout=None, return_when=ALL_COMPLETED):
"""Wait for the futures in the given sequence to complete.
Args:
fs: The sequence of Futures (possibly created by different Executors) to
wait upon.
timeout: The maximum number of seconds to wait. If None, then there
is no limit on the wait time.
return_when: Indicates when this function should return. The options
are:
FIRST_COMPLETED - Return when any future finishes or is
cancelled.
FIRST_EXCEPTION - Return when any future finishes by raising an
exception. If no future raises an exception
then it is equivalent to ALL_COMPLETED.
ALL_COMPLETED - Return when all futures finish or are cancelled.
Returns:
A named 2-tuple of sets. The first set, named 'done', contains the
futures that completed (is finished or cancelled) before the wait
completed. The second set, named 'not_done', contains uncompleted
futures.
"""
with _AcquireFutures(fs):
done = set(f for f in fs
if f._state in [CANCELLED_AND_NOTIFIED, FINISHED])
not_done = set(fs) - done
if (return_when == FIRST_COMPLETED) and done:
return DoneAndNotDoneFutures(done, not_done)
elif (return_when == FIRST_EXCEPTION) and done:
if any(f for f in done
if not f.cancelled() and f.exception() is not None):
return DoneAndNotDoneFutures(done, not_done)
if len(done) == len(fs):
return DoneAndNotDoneFutures(done, not_done)
waiter = _create_and_install_waiters(fs, return_when)
waiter.event.wait(timeout) # 等待未完成的期物执行计算
for f in fs:
with f._condition:
f._waiters.remove(waiter)
done.update(waiter.finished_futures)
return DoneAndNotDoneFutures(done, set(fs) - done) # DoneAndNotDoneFutures 是一个命名元组,包含两个字段,第一个字段表示状态为完成的期物集合, 第二个表示状态为pending的期物集合。
从源码来看, wait 与 as_complete 功能比较类似, 都是为获取完成状态的期物, 只是实现方式略有不同。
小结
- 将期物对象理解为异步计算任务的一个容器,异步计算的结果通过期物的 set_result 或者 set_exception 写入期物对象中,set_result 方法中需要重点关注 waiter 对象,waiter 对象内部有一个 threading.Event 对象,通过 Event 对象的 wait 与 set 方法,在 as_complete 或者 wait 函数中,使开发者能够及时获取完成状态的期物结果。
- _WorkItem 对象将期物、待执行的函数封装在一起,其 run 方法会先执行函数计算,然后将函数计算的结果写入期物中。
- 线程池执行器中的 submit 函数充当了 生成者的角色,通过 submit, 提交待计算的任务,待计算的任务封装成 _WorkItem 对象,存储在线程池执行器的 _work_queue 队列中。 _worker 函数在线程池执行器中扮演者消费者的角色, 最多允许 max_workers 个消费线程,_worker 函数会不停的从 _work_queue 队列取出 _WorkItem 对象开始计算。
- submit 方法是非阻塞的,通过 as_complete 或者 wait 函数, 可以获取到所需完成状态的期物对象,从而获取到计算结果, as_complete 或者 wait 函数的核心就是 waiter 对象。而 waiter 对象会存储在期物的 _waiters 列表属性中。