多处理中的多线程有意义吗?
使用 Python 的多处理,在Pool
其中包含一堆ThreadPool
s是否有意义?说我有这样的事情:
def task(path):
# i/o bound
image = load(path)
# cpu bound but only takes up 1/10 of the time of the i/o bound stuff
image = preprocess(img)
# i/o bound
save(image, path)
然后我想处理一个路径列表path_list
。如果我使用,ThreadPool
我仍然会因为 cpu 绑定位而达到天花板。如果我使用 a ,Pool
我会花太多时间等待 i/o。那么最好将path_list
多个进程拆分为每个进程使用多个线程吗?
重申我的示例的另一种更简短的方法是,如果我有一个方法应该是多线程的,因为它是 i/o 绑定的,但我也想使用许多 cpu 内核怎么办?如果我使用 a,Pool
我会将每个核心用于 I/O 绑定的单个任务。如果我使用一个,ThreadPool
我只能使用一个核心。
回答
TL; 博士
在这种情况下
在这种情况下,没有。在多个进程中使用多个线程运行此任务将花费您不必要的开销。您的任务由使用不同硬件组件的步骤组成。每个步骤都需要不同数量的线程/进程来运行以实现最大吞吐量,而这种设计使您为所有步骤分配相同数量的资源来限制您。您最终可能会使用比您需要的更多的进程/线程,并支付上下文切换、内存使用和缓存未命中的费用。
一种可以减少这种开销并让您更好地控制资源使用的解决方案是将您的任务拆分为一个管道,并为每个步骤分配足够数量的硬件/操作系统资源。有关更详细的设计建议,请参阅下面的“3. 设计解决方案以实现该吞吐量”。
在其他情况下
在某些情况下,这种方法(进程池中的线程池)可能是一个不错的选择。想象一下必须在 python 中执行大量 IO 绑定任务的工作负载。使用线程是有意义的,因为进程会产生更多的开销。但是如果你有足够多的并发任务,你会开始看到很多线程在等待 GIL,这意味着延迟增加,CPU 甚至可能变成瓶颈。在这种情况下,使用更多进程是有意义的,每个进程都有另一个线程池,并获得更多 CPU 时间(假设您有可用内核)。
为实现最大性能而规划
从现在开始,我假设您的目标是最大限度地利用您的硬件,以实现“任务”的最大吞吐量。在这种情况下,您的问题的答案很大程度上取决于您的硬件,并且需要进行一些测量。我建议:
- 了解您的硬件利用率
- 识别瓶颈并估计最大吞吐量
- 设计解决方案以实现该吞吐量
- 实施设计并进行优化,直到满足您的要求
1. 了解您的硬件利用率
在这种情况下,涉及到一些硬件:
- 内存
- 磁盘
- 中央处理器
让我们看一个“任务”并注意它如何使用硬件:
- 磁盘(读取)
- 内存(写入)
- CPU时间
- 内存(读取)
- 磁盘(写入)
2. 识别瓶颈并估计最大吞吐量
为了识别瓶颈,让我们计算每个硬件组件可以提供的任务的最大吞吐量,假设它们的使用可以完全并行化。我喜欢使用 python 来做到这一点:(请注意,我使用的是随机常量,您必须为您的设置填写真实数据才能使用它)。
# ----------- General consts
input_image_size = 20 * 2 ** 20 # 20MB
output_image_size = 15 * 2 ** 20 # 15MB
# ----------- Disk
# If you have multiple disks and disk access is the bottleneck, you could split the images between them
amount_of_disks = 2
disk_read_rate = 3.5 * 2 ** 30 # 3.5GBps, maximum read rate for a good SSD
disk_write_rate = 2.5 * 2 ** 30 # 2.5GBps, maximum write rate for a good SSD
disk_read_throughput = amount_of_disks * disk_read_rate / input_image_size
disk_write_throughput = amount_of_disks * disk_write_rate / output_image_size
# ----------- RAM
ram_bandwidth = 30 * 2 ** 30 # Assuming here similar write and read rates of 30GBps
# assuming you are working in userspace and not using a userspace filesystem,
# data is first read into kernel space, then copied to userspace. So in total,
# two writes and one read.
userspace_ram_bandwidth = ram_bandwidth / 3
ram_read_throughput = userspace_ram_bandwidth / input_image_size
ram_write_throughput = userspace_ram_bandwidth / output_image_size
# ----------- CPU
# We decrease one core, as at least some scheduling code and kernel code is going to run
core_amount = 8 - 1
# The measured amount of times a single core can run the preprocess function in a second.
# Assuming that you are not planning to optimize the preprocess function as well.
preprocess_function_rate = 1000
cpu_throughput = core_amount * preprocess_function_rate
# ----------- Conclusions
min_throughput, bottleneck_name = min([(disk_read_throughput, 'Disk read'),
(disk_write_throughput, 'Disk write'),
(ram_read_throughput, 'RAM read'),
(ram_write_throughput, 'RAM write'),
(cpu_throughput, 'CPU')])
cpu_cores_needed = min_throughput / preprocess_function_rate
print(f'Throughput: {min_throughput:.1f} tasks per secondn'
f'Bottleneck: {bottleneck_name}n'
f'Worker amount: {cpu_cores_needed:.1f}')
此代码输出:
Throughput: 341.3 tasks per second
Bottleneck: Disk write
Worker amount: 0.3
这意味着:
- 我们可以达到的最大速率约为每秒 341.3 个任务
- 磁盘是瓶颈。例如,您可以通过以下方式提高性能:
- 购买更多磁盘
- 使用
ramfs
或类似的解决方案避免完全使用磁盘
- 在
task
并行执行所有步骤的系统中,您不需要专门用于运行preprocess
. (在 python 中,这意味着你可能只需要一个进程,线程或 asyncio 就足以实现与其他步骤的并发)
注意:数字在说谎
这种估计很难正确。很难不忘记计算本身中的事情,并且很难对常数进行良好的测量。例如,当前计算存在一个大问题——读取和写入不是正交的。我们在计算中假设一切都是并行发生的,因此disk_read_rate
必须考虑与读取同时发生的写入等常量。RAM 速率应该至少降低 50%。
3. 设计一个解决方案来实现该吞吐量
与您在问题中提供的内容类似,我的初始设计类似于:
- 让一组工作人员加载图像并将它们发送到队列中以进行下一步(我们需要使用多个内核进行读取以使用所有可用的内存带宽)
- 让一组工作人员处理图像并将结果发送到队列中(工作人员的数量应根据上面脚本的输出选择。对于当前结果,数量为 1)
- 让一组工作人员将处理后的图像保存到磁盘。
实际的实施细节将根据您在实施解决方案时遇到的不同技术限制和开销而有所不同。如果没有进一步的细节和测量,很难猜测它们到底是什么。
4. 实施设计,并进行优化,直到满足您的要求
祝你好运,但请注意,即使您在估计最大吞吐量方面做得很好,也可能很难达到。将最大速率与您的速度要求进行比较可能会让您很好地了解所需的工作量。例如,如果您需要的速率比最大速率慢 10 倍,您可能会很快完成。但如果它只慢 2 倍,您可能需要考虑将硬件加倍并开始为一些艰苦的工作做准备:)
- Very well written, thank you sir