Python 为什么要用线程池?

一、为什么要使用线程池

多线程的情况下确实可以最大限度发挥多核处理器的计算能力,提高系统的吞吐量和性能。

但是如果随意使用多线程,对系统的性能反而有不利影响。

比如下面的情况:

创建线程是需要时间的,假设线程创建所需时间为T1,线程执行任务时间为T2,线程销毁时间为T3,而往往T1+T3>T2。所以频繁创建和销毁线程也会消耗大量的时间。

如果有任务来了,再去创建线程的话效率比较低。

其次线程也需要占用内存空间,线程池可以管理控制线程,线程是稀缺资源,如果无休止的创建会消耗系统资源,还会降低系统稳定性。

因此,为了避免频繁的创建和销毁线程,让创建的线程进行复用,就有了线程池的概念。

使用线程池可以进行统一分配,方便调优和监控。

线程池里会维护一部分活跃线程,如果有需要,就去线程池里取线程使用,用完即归还到线程池里,免去了创建和销毁线程的开销,且线程池也会线程的数量有一定的限制。

二、线程池的使用

线程池的基类是 concurrent.futures 模块中的 Executor。

Executor 提供了两个子类,即 ThreadPoolExecutor 和 ProcessPoolExecutor。

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

如果使用线程池/进程池来管理并发编程,那么只要将相应的 task 函数提交给线程池/进程池,剩下的事情就由线程池/进程池来搞定。

Exectuor 提供了如下常用方法:

  1. submit
submit(fn, *args, **kwargs)

submit 将 fn 函数提交给线程池。

*args 代表传给 fn 函数的参数。

*kwargs 代表以关键字参数的形式为 fn 函数传入参数。

  1. map
map(func, *iterables, timeout=None, chunksize=1)

map 函数类似于全局函数 map(func, *iterables),只是该函数将会启动多个线程,以异步方式立即对 iterables 执行 map 处理。

  1. 关闭线程池。
shutdown(wait=True)

程序将函数提交(submit)给线程池后,submit 方法会返回一个对象,该对象主要用于获取线程任务函数的返回值。

该对象提供了如下方法:

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

在用完一个线程池后,应该调用该线程池的 shutdown() 方法,该方法将启动线程池的关闭序列。

调用 shutdown() 方法后的线程池不再接收新任务,但会将以前所有的已提交任务执行完成。

当线程池中的所有任务都执行完成后,该线程池中的所有线程都会死亡。

使用线程池来执行线程任务的步骤

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

def action(max):
    sum = 0
    for i in range(max):
        print(threading.current_thread().name+' '+str(i))
        sum += i
    return sum

# 创建一个包含2个线程的线程池
pool = ThreadPoolExecutor(max_workers=2)

# 向线程池提交一个任务
future1 = pool.submit(action, 3)
# 向线程池再提交一个任务
future2 = pool.submit(action, 5)
# 判断 future1 的任务是否结束
print(future1.done())
time.sleep(2)
# 判断 future2 的任务是否结束
print(future2.done())
# 查看future1代表的任务返回的结果
print(future1.result())
# 查看future2代表的任务返回的结果
print(future2.result())
# 关闭线程池
pool.shutdown()

运行结果:

ThreadPoolExecutor-0_0 0
ThreadPoolExecutor-0_0 1
ThreadPoolExecutor-0_1 0
ThreadPoolExecutor-0_1 1
ThreadPoolExecutor-0_1 2
ThreadPoolExecutor-0_1 3
ThreadPoolExecutor-0_1 4
False
ThreadPoolExecutor-0_0 2
True
3
10

总结:

线程池在系统启动时即创建一些空闲的线程,程序只要将一个函数提交给线程池,线程池就会启动一个空闲的线程来执行它。

当该函数执行结束后,该线程并不会死亡,而是再次返回到线程池中变成空闲状态,等待执行下一个函数。

此外,使用线程池可以有效地控制系统中并发线程的数量。当系统中包含有大量的并发线程时,会导致系统性能急剧下降,甚至导致 Python 解释器崩溃,而线程池的最大线程数参数可以控制系统中并发线程的数量不超过此数。


编程的朝圣之路

posted @ 2020-09-30 08:05  小名叫小明  阅读(577)  评论(0编辑  收藏  举报