Python多线程编程《四》:ThreadPoolExecutor 与 Future

标准库 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 列表属性中。
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值