ThreadPoolExecutor是Python标准库concurrent.futures中的一个类,它提供了一种方便的方式来使用线程池,从而实现并发执行任务的目的。使用ThreadPoolExecutor可以避免手动管理线程的复杂性,同时可以利用现代CPU的多核心能力,提高程序的运行效率。
ThreadPoolExecutor 会维护一个线程池,当有任务提交时,它会分配一个空闲的线程来执行任务。如果没有空闲线程可用,则任务将等待,直到有线程可用。一旦线程池中的任务全部完成,线程池将保持空闲状态,等待下一批任务的到来。
1.创建进程池
创建一个ThreadPoolExecutor对象。
max_workers 可以指定最大工作线程数,如果不指定,则默认为系统CPU核心数。
from concurrent.futures import ThreadPoolExecutor
executor = ThreadPoolExecutor(max_workers=3)
2.向进程池中提交任务
submit(fn, *args, **kwargs)用于向线程池中提交任务。
submit()方法用于向进程池中提交一个可调用对象。fn是可调用对象,*args和**kwargs是fn的位置参数和关键字参数。submit()方法返回一个Future对象,可以用来获取任务的结果。如果fn引发了异常,异常信息会被存储在Future对象中,可以使用Future对象的exception()方法获取异常信息。
def func(arg):
print(f"Task {arg} is running")
return arg * 2
future = executor.submit(func, 1)
3.获取任务的结果
Future 对象可以用来获取任务的执行结果。可以使用 Future 对象的 result() 方法来等待任务执行完成并获取其结果。result() 方法会阻塞当前线程,直到任务执行完成并返回结果。
result = future.result()
4.批量提交任务
如果要向进程池中提交多个任务,可以使用进程池对象的 map() 方法。
map(func, *iterables, timeout=None, chunksize=1)
map()方法用于批量提交任务。func是可调用对象,*iterables是一个或多个可迭代对象,每个可迭代对象的元素都会作为func的参数。timeout是超时时间,chunksize指定每个子进程执行的任务数量。
results = executor.map(func, [1, 2, 3])
这里使用 map() 方法向进程池中提交了三个任务。map() 方法会立即返回一个迭代器对象 results,该对象可以用来获取每个任务的执行结果。例如:
for result in results:
# 处理结果
pass
这里使用迭代器对象 results 获取每个任务的执行结果,并进行处理。如果有任务还没有执行完成,迭代器会一直阻塞,直到该任务执行完成并返回结果。
5.关闭进程池
使用进程池对象的 shutdown() 方法可以关闭进程池。shutdown() 方法用于关闭线程池。它会等待所有已提交的任务完成后再停止线程池。如果在调用 shutdown() 方法前已经调用了 shutdown(wait=True) 方法,则 shutdown() 方法会立即返回,而不会等待任务完成。
executor.shutdown()
6.其他常用方法
wait(fs, timeout=None, return_when=ALL_COMPLETED)
wait()方法用于等待一组Future对象执行完毕。fs是一组Future对象,timeout是超时时间,return_when指定何时返回。如果指定return_when为FIRST_COMPLETED,则在第一个Future对象完成后就会返回。
as_completed(fs, timeout=None)
as_completed()方法用于获取已完成的Future对象。fs是一组Future对象,timeout是超时时间。as_completed()方法返回一个迭代器,每次迭代都会返回一个已完成的Future对象。
7.使用上下文管理器
上下文管理器可以确保进程池在使用完毕后自动关闭,释放资源,避免资源泄漏。
当with块执行完毕时,进程池会自动关闭,释放资源。
with ThreadPoolExecutor(max_workers=3) as executor:
future = executor.submit(func, 1)
result = future.result()
print(f"Task result is {result}")
ThreadPoolExecutor可以充分利用多核CPU的优势,实现并行执行任务的目的。现代CPU通常都有多个核心,可以同时执行多个任务,从而提高程序的性能和效率。
Python中的全局解释器锁(GIL)是一种机制,用于确保同一时间只有一个线程执行Python字节码。这个机制可以避免数据竞争和一些其他的并发问题,但是它也会限制Python程序的并行性能。ThreadPoolExecutor可以避免GIL的限制,实现真正的并行执行,从而提高程序的并发能力和性能。
max_workers的设置
ThreadPoolExecutor可以指定最大工作线程数,如果不指定,则默认为系统CPU核心数。在设置最大工作线程数时,需要根据实际情况进行调整,避免创建过多的线程,导致系统资源不足和线程竞争的问题。
任务阻塞
如果在任务中使用了阻塞式的IO操作或者其他阻塞式的操作,可能会导致线程池中的其他任务被阻塞,从而影响程序的性能和效率。在这种情况下,可以考虑使用异步IO或者其他非阻塞式的操作。
处理任务执行异常
遇到任务执行异常可以使用Future对象的exception()方法获取异常信息,或者使用try/except语句捕获异常并进行处理。
线程池的关闭
任务执行完成后及时关闭线程池,释放资源,避免资源泄漏。使用shutdown()方法关闭线程池,或者使用上下文管理器来自动关闭线程池。
线程安全问题
在多线程并发执行任务时,需要注意线程安全。例如多个线程同时对同一个共享变量进行读写操作可能会导致数据竞争和一些其他的并发问题。在这种情况下,可以使用锁等机制来解决线程安全问题。
线程 vs 进程:ThreadPoolExecutor 使用线程池来执行任务,而 ProcessPoolExecutor 使用进程池来执行任务。因为线程是在同一个进程内运行的,所以线程之间共享了进程的内存空间,这意味着线程之间的通信和数据共享比进程更容易。但是,由于 Python 的全局解释器锁(GIL)的存在,同一时刻只有一个线程可以执行 Python 代码,这意味着在 CPU 密集型任务中,多线程并不能真正发挥多核 CPU 的优势。而进程之间是相互独立的,不受 GIL 的限制,可以充分利用多核 CPU 资源。
适用场景:由于 GIL 的限制,ThreadPoolExecutor 更适合于 I/O 密集型任务,如网络请求、文件读写等操作,而 ProcessPoolExecutor 更适合于 CPU 密集型任务,如大量计算、图像处理等操作。
系统资源消耗:由于进程之间是相互独立的,ProcessPoolExecutor 需要更多的系统资源来维护进程之间的通信和数据共享,而 ThreadPoolExecutor 只需要维护线程之间的通信和数据共享,所以在系统资源有限的情况下,ThreadPoolExecutor 更加适合。