Python 线程池

最近项目中大量使用到了异步编程,于是参考了大量关于 python 的异步编程实践,最终选择了将所有方法传入线程池,使用线程池来执行的方案。

线程池的优点

  1. 系统启动一个新线程的成本是比较高的,因为它涉及与操作系统的交互。而使用线程池控制线程数量,可以很好地提升性能。
  2. 使用线程池时,可以复用空闲的线程,避免线程爆炸,并且方便管理。
  3. 使用线程池可以控制并发线程的数量。当系统中有大量的并发线程时,会导致系统性能急剧下降,甚至导致 Python 解释器崩溃,而线程池的最大线程数可以控制系统中并发线程的数量不超过此数。

python中的池化异步编程

在 python3 中,concurrent.futures 是封装专门用来进行异步编程一些非常好用的方法和类。异步池的基类是:from concurrent.futures import Executor。Executor有两个子类,即 ThreadPoolExecutor 和 ProcessPoolExecutor。

其中 ThreadPoolExecutor 用于创建线程池,而 ProcessPoolExecutor 用于创建进程池。

一般来说,两者都可用于异步解耦编程。当程序属于cpu密集型时,可以优先考虑 ProcessPoolExecutor 进程池,当程序属于io密集型时,可以优先考虑使用 ThreadPoolExecutor。

这次我们主要介绍 ThreadPoolExecutor 线程池的使用方法,不涉及过多源码的介绍,源码的介绍另外用一篇文章记录和介绍。

Future

所有传给 Executor 的异步函数,都会以 Future 类的形式存在,由于线程任务会在新线程中以异步方式执行,因此,线程执行的函数相当于一个“将来完成”的任务,所以 Python 使用 Future 来代表。

Future 类主要有以下几种方法:

  • cancel():取消该 Future 代表的线程任务。如果该任务正在执行,不可取消,则该方法返回 False;否则,程序会取消该任务,并返回 True。
  • cancelled():返回 Future 代表的线程任务是否被成功取消。
  • running():如果该 Future 代表的线程任务正在执行、不可被取消,该方法返回 True。
  • done():如果该 Future 代表的线程任务被成功取消或执行完成,则该方法返回 True。
  • result(timeout=None):获取该 Future 代表的线程任务最后返回的结果。如果 Future 代表的线程任务还未完成,该方法将会阻塞当前线程,其中 timeout 参数指定最多阻塞多少秒。
  • exception(timeout=None):获取该 Future 代表的线程任务所引发的异常。如果该任务成功完成,没有异常,则该方法返回 None。
  • add_done_callback(fn):为该 Future 代表的线程任务注册一个“回调函数”,当该任务成功完成时,程序会自动触发该 fn 函数。

Executor

不管是 ThreadPoolExecutor 还是 ProcessPoolExecutor,都是继承自 Executor 抽象类,在 Executor 中主要提供了以下常用的方法:

  • submit(fn):将需要执行的 fn 函数提交给线程池。

  • map(fn,*iter): 该函数将会启动多个线程,以异步方式立即对 iter 执行 map 处理,每个项异步执行 fn 函数。

  • shutdown():关闭线程池。调用后的线程池不再接收新任务,但会将以前所有的已提交任务执行完成。当线程池中的所有任务都执行完成后,该线程池中的所有线程都会死亡。

线程池执行步骤

  1. 调用 ThreadPoolExecutor 创建一个线程池。
  2. 定义一个普通函数作为线程任务。
  3. 调用 ThreadPoolExecutor 的 submit() 方法来提交线程任务。
  4. 当不想提交任何任务时,调用 ThreadPoolExecutor 对象的 shutdown() 方法来关闭线程池

ThreadPoolExecutor实践

submit

下面写一个简单的例子,体现通过 ThreadPoolExecutor 异步编程的主要方式。

from concurrent import futures


def test_fn(a):
    print(a)
    if a == 20:
        raise ValueError("这个有错")


def test_callback(future: futures.Future):
    fn_exception = future.exception()
    if fn_exception:
        print(fn_exception)


with futures.ThreadPoolExecutor() as pool:
    for i in range(50):
        pool.submit(test_fn, i).add_done_callback(test_callback)

Executor 实现了ptyhon的上下文管理协议,我们可以直接通过 with 的方式启动线程池,就不需要刻意关心线程池的创建和销毁,with会帮我们处理。

代码中,我们通过 submit 方法将 test_fn 函数提交给线程池执行,该线程池就会负责启动线程来执行 test_fn 函数。这种启动线程的方法既优雅,又具有更高的效率。

submit 方法调用后会返回一个 Future 对象,我们可以使用上面 Future 对象的主要方法对该线程的任务进行后续的管理。在示例里,我们用了 add_done_callback 回调函数的形式;当该线程的任务结束后,就会调用传给 add_done_callback 的 test_callback 函数。

在 test_callback 中,可以写自己接受回调函数的逻辑,在示例中,我们使用了 future.exception 捕捉异常,如果线程任务正常结束就跳过,如果有异常,则打印异常,完成异常的捕捉。

map

上面是通过 submit 单个提交线程任务,当有大量相同的任务,而只是传入的参数不一样时,map 方法就非常有用了,下面展示了用 map 方法执行同一个 test_fn 函数的方式。

from concurrent import futures

def test_fn(a):
    if a == 20:
        return None
    return a


with futures.ThreadPoolExecutor() as pool:
    a = [i for i in range(50)]
    results = pool.map(test_fn, a)
    for r in results:
        print(r)

通过上面程序可以看出,使用 map 方法来启动线程,并收集线程的执行结果,不仅具有代码简单的优点,而且虽然程序会以并发方式来执行 test_fn 函数,但最后收集的 test_fn 函数的执行结果,依然与传入参数的结果保持一致,不会导致乱序,这点很有用。

总结

如果我们需要启动多线程来执行函数的话,那么不妨使用线程池。每调用一个函数就从池子里面取出一个线程,函数执行完毕就将线程放回到池子里以便其它函数执行,既高效又优雅。

你可能感兴趣的:(Python模块使用,python,开发语言)