- CPython 实现细节: 在 CPython 中,由于存在 全局解释器锁,同一时刻只有一个线程可以执行 Python 代码(虽然某些性能导向的库可能会去除此限制)。 如果你想让你的应用更好地利用多核心计算机的计算资源,推荐你使用 multiprocessing 或 concurrent.futures.ProcessPoolExecutor。 但是,如果你想要同时运行多个 I/O 密集型任务,则多线程仍然是一个合适的模型。
- concurrent.futures.ProcessPoolExecutor 提供了一个更高层级的接口用来将任务推送到后台进程而不会阻塞调用方进程的执行。 与直接使用 Pool 接口相比,concurrent.futures API 能更好地允许将工作单元发往无需等待结果的下层进程池。
- concurrent.futures 模块提供异步执行可调用对象高层接口。这是今天的重点。因为我的代码使用到了它。异步执行可以由 ThreadPoolExecutor 使用线程或由 ProcessPoolExecutor 使用单独的进程来实现。 两者都是实现抽象类 Executor 定义的接口。
ThreadPoolExecutor
和ProcessPoolExecutor
都是Python的concurrent.futures
模块中的执行器(Executor),它们用于创建不同类型的工作池来执行并行任务。它们之间的主要区别在于它们如何处理任务和使用系统资源:
ThreadPoolExecutor:
- 使用线程池来执行任务。
- 每个工作线程属于同一个进程,可以共享内存和资源。
- 适合I/O密集型任务,例如文件读写、网络通信等,因为这些任务大部分时间在等待外部资源,而不是持续占用CPU
ProcessPoolExecutor:
- 使用进程池来执行任务。
- 每个工作进程是独立的,有自己的内存空间,不与其他进程共享状态。
- 适合CPU密集型任务,例如复杂计算、数据处理等,因为它可以绕过全局解释器锁(GIL),允许多个进程同时使用多个CPU核心
with ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
results, dir_count, file_count, size_sum = executor.map(tree, Path(directory).iterdir())
# Write results to Excel file after processing all subdirectories
write_to_excel(results, output_file_path)
(补充:在使用ThreadPoolExecutor
和executor.map
进行并行遍历时,需要一些逻辑来确保不会有重复的遍历和结果。1、如果有4个executor
实例并行执行tree
函数,为了避免混乱,需要确保每个实例处理的目录范围不重叠。这通常通过将目录分割成不同的子目录,并将每个子目录分配给不同的executor
实例来实现。2、如果正确地分配了任务,results
中不应该有重复的内容。每个executor
实例应该返回其独立处理的目录的结果。这是一个风险)
在选择使用ThreadPoolExecutor
还是ProcessPoolExecutor
时,需要考虑任务的性质:
- 如果任务主要是等待外部资源,如网络请求或磁盘I/O,那么使用
ThreadPoolExecutor
更合适,因为线程切换的开销比进程小,且可以共享内存。 - 如果任务是计算密集型的,需要大量的CPU时间,那么使用
ProcessPoolExecutor
更合适,因为它可以充分利用多核处理器的优势,避免GIL的限制。
总的来说,选择哪种执行器取决于任务的类型和需要优化的资源(CPU还是I/O)。
二、ThreadPoolExecutor
的submit
方法和map
方法都用于提交任务给线程池执行,但它们在处理任务时有一些关键的区别:
-
任务提交方式:
submit
方法是非阻塞的,它会立即返回一个Future
对象,代表异步执行的操作。您可以使用这个Future
对象来查询任务的状态和结果。map
方法则是阻塞的,它会等待所有任务完成,并返回一个结果列表。这使得map
方法更适合批量处理那些相互独立且耗时相似的任务。
-
结果处理:
- 使用
submit
方法时,您需要手动处理每个Future
对象以获取结果。 map
方法会自动处理结果,按照任务提交的顺序返回结果。
- 使用
-
异常处理:
submit
方法允许您对每个Future
对象单独进行异常处理。map
方法会等到所有任务尝试执行完毕后,才会抛出第一个遇到的异常。
-
负载均衡:
submit
方法更灵活,适合于负载不均匀的情况。您可以在任务完成后继续向线程池提交新任务,实现动态的负载均衡。map
方法则是一次性提交所有任务,适用于负载相对均匀的场景。
总的来说,如果需要更细粒度的控制,或者任务的复杂度和耗时各不相同,那么submit
方法可能是更好的选择。如果您有一组相似的任务需要批量处理,并且希望简化结果的处理,那么map
方法可能更适合。
三、线程(Thread)和进程(Process)是计算机操作系统中用于执行任务的两种基本单位。它们之间的关系和区别如下:
-
关系:
- 线程是进程的一部分,一个进程可以包含一个或多个线程。
- 线程是进程中的实际执行单位,它们共享进程的资源,如内存和文件句柄。
-
区别:
-
进程:
- 进程是操作系统分配资源和调度的基本单位。
- 每个进程都有独立的地址空间和资源,进程间不共享内存1。
- 进程间的通信(IPC)需要特定的机制,如管道、信号量、共享内存等。
- 进程切换的开销较大,因为涉及到完整的上下文切换和资源重新分配。
- 进程更适合于需要资源隔离和独立运行的任务。
-
线程:
- 线程是进程中的执行路径,是CPU调度的基本单位。
- 同一进程内的线程共享内存和资源,线程间切换的开销较小1。
- 线程间的通信更为方便,因为它们可以直接读写共享内存。
- 线程更适合于I/O密集型和需要频繁交互的任务。
-
总的来说,进程提供了资源隔离和安全性,而线程提供了执行效率和方便的通信方式。在设计应用程序时,根据任务的特点选择使用进程还是线程非常重要。