Python中的多处理和多线程

Python中的多处理和多线程,python,multitasking,Python,Multitasking,我有一个python程序,它1)从磁盘读取一个非常大的文件(大约95%的时间),然后2)处理并提供相对较小的输出(大约5%的时间)。该程序将在TB的文件上运行 现在我希望通过利用多处理和多线程来优化这个程序。我运行的平台是一个虚拟机,在一个虚拟机上有4个处理器 我计划有一个调度程序进程,它将执行4个进程(与处理器相同),然后每个进程都应该有一些线程,因为大部分是I/O。每个线程将处理1个文件,并将结果报告给主线程,主线程将通过IPC将结果报告给调度程序进程。调度器可以对这些数据进行排队,并最终按

我有一个python程序,它1)从磁盘读取一个非常大的文件(大约95%的时间),然后2)处理并提供相对较小的输出(大约5%的时间)。该程序将在TB的文件上运行

现在我希望通过利用多处理和多线程来优化这个程序。我运行的平台是一个虚拟机,在一个虚拟机上有4个处理器

我计划有一个调度程序进程,它将执行4个进程(与处理器相同),然后每个进程都应该有一些线程,因为大部分是I/O。每个线程将处理1个文件,并将结果报告给主线程,主线程将通过IPC将结果报告给调度程序进程。调度器可以对这些数据进行排队,并最终按顺序将它们写入磁盘

所以,想知道如何确定为这种场景创建的进程和线程的数量?有没有一种数学方法可以计算出什么是最佳组合

感谢您的并行处理: 我看到并引用了公认的答案:

在实践中,可能很难找到最佳线程数,甚至在每次运行程序时,该线程数也可能会有所不同。因此,理论上,线程的最佳数量将是您机器上的内核数量。如果您的内核是“超线程”(Intel称之为“超线程”),那么它可以在每个内核上运行2个线程。那么,在这种情况下,线程的最佳数量是机器上内核数量的两倍

对于多处理: 有人问了一个类似的问题,被接受的答案是:

如果您的所有线程/进程确实都是CPU绑定的,那么您应该运行与CPU报告核心数量相同的进程。由于超线程,每个物理CPU核可能能够呈现多个虚拟核。调用
multiprocessing.cpu\u count
获取虚拟核的数量

如果只有1个线程中的p是CPU限制的,那么可以通过乘以p来调整该数字。例如,如果有一半的进程是CPU限制的(p=0.5),并且有两个CPU,每个CPU有4个内核和2个超线程,则应该启动0.5*2*4*2=8个进程

这里的关键是了解您使用的是哪台机器,从中,您可以选择接近最佳数量的线程/进程来分割代码的执行。我说的几乎是最优的,因为每次运行脚本时它都会有一点变化,所以很难从数学的角度预测这个最优值

对于您的特定情况,如果您的机器有4个内核,我建议您最多只创建4个线程,然后拆分它们:

  • 1到主线程
  • 3用于文件读取和处理

使用多个进程来加速IO性能可能不是一个好主意,请查看下面的部分,看看它是否有用。

一个想法是让线程只读取文件(如果我理解得很好,只有一个文件)并将独立部分(例如行)推入消息队列

消息可以由4个线程处理。通过这种方式,您可以优化处理器之间的负载。

我想我会按照与您所做相反的方式来安排它。也就是说,我将创建一个特定大小的线程池,负责生成结果。提交到此池的任务将作为参数传递给处理器池,工作线程可以使用该处理器池提交CPU绑定的部分工作。换句话说,线程池工作人员主要负责执行所有与磁盘相关的操作,并将CPU密集型工作交给处理器池

处理器池的大小应该是环境中的处理器数量。很难给出线程池的精确大小;这取决于在收益递减定律发挥作用之前,它能处理多少并发磁盘操作。这还取决于您的内存:池越大,占用的内存资源就越多,尤其是在必须将整个文件读入内存进行处理的情况下。因此,您可能需要对该值进行实验。下面的代码概述了这些想法。从线程池中获得的是I/O操作的重叠,这比仅使用小型处理器池时要大:

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
from functools import partial
import os

def cpu_bound_function(arg1, arg2):
    ...
    return some_result



def io_bound_function(process_pool_executor, file_name):
    with open(file_name, 'r') as f:
        # Do disk related operations:
        . . . # code omitted
        # Now we have to do a CPU-intensive operation:
        future = process_pool_executor.submit(cpu_bound_function, arg1, arg2)
        result = future.result() # get result
        return result
    
file_list = [file_1, file_2, file_n]
N_FILES = len(file_list)
MAX_THREADS = 50 # depends on your configuration on how well the I/O can be overlapped
N_THREADS = min(N_FILES, MAX_THREADS) # no point in creating more threds than required
N_PROCESSES = os.cpu_count() # use the number of processors you have

with ThreadPoolExecutor(N_THREADS) as thread_pool_executor:
    with ProcessPoolExecutor(N_PROCESSES) as process_pool_executor:
        results = thread_pool_executor.map(partial(io_bound_function, process_pool_executor), file_list)
重要注意事项

另一个简单得多的方法是只拥有一个处理器池,其大小大于您拥有的CPU处理器数量,例如25个。工作进程将执行I/O和CPU操作。即使您的进程比CPU多,但许多进程仍将处于等待状态,等待I/O完成,从而允许运行CPU密集型工作

这种方法的缺点是,创建N个进程的开销远远大于创建N个线程+少量进程的开销。但是,随着提交到池中的任务的运行时间越来越长,增加的开销在总运行时间中所占的百分比也越来越小。因此,如果您的任务不是琐碎的,那么这可能是一个合理的性能简化

更新:两种方法的基准

我对处理24个大小约为10000KB的文件的两种方法做了一些基准测试(实际上,这些文件仅处理了3个不同的文件,每个文件处理了8次,因此可能进行了一些缓存):

方法1(线程池+处理器池)

方法2(仅处理器池)

结果:

(我有8个核)

线程池+处理器池:13.5秒 仅处理器池:13.3秒

结论:首先,我会尝试一种更简单的方法,即使用处理器池来处理所有事情。现在棘手的一点是决定要创建的最大进程数,这是您原始问题的一部分,并且在所有
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
from functools import partial
import os
from math import sqrt
import timeit


def cpu_bound_function(b):
    sum = 0.0
    for x in b:
        sum += sqrt(float(x))
    return sum

def io_bound_function(process_pool_executor, file_name):
    with open(file_name, 'rb') as f:
        b = f.read()
        future = process_pool_executor.submit(cpu_bound_function, b)
        result = future.result() # get result
        return result

def main():
    file_list = ['/download/httpd-2.4.16-win32-VC14.zip'] * 8 + ['/download/curlmanager-1.0.6-x64.exe'] * 8 + ['/download/Element_v2.8.0_UserManual_RevA.pdf'] * 8
    N_FILES = len(file_list)
    MAX_THREADS = 50 # depends on your configuration on how well the I/O can be overlapped
    N_THREADS = min(N_FILES, MAX_THREADS) # no point in creating more threds than required
    N_PROCESSES = os.cpu_count() # use the number of processors you have

    with ThreadPoolExecutor(N_THREADS) as thread_pool_executor:
        with ProcessPoolExecutor(N_PROCESSES) as process_pool_executor:
            results = list(thread_pool_executor.map(partial(io_bound_function, process_pool_executor), file_list))
            print(results)

if __name__ == '__main__':
    print(timeit.timeit(stmt='main()', number=1, globals=globals()))
from concurrent.futures import ProcessPoolExecutor
from math import sqrt
import timeit


def cpu_bound_function(b):
    sum = 0.0
    for x in b:
        sum += sqrt(float(x))
    return sum

def io_bound_function(file_name):
    with open(file_name, 'rb') as f:
        b = f.read()
        result = cpu_bound_function(b)
        return result

def main():
    file_list = ['/download/httpd-2.4.16-win32-VC14.zip'] * 8 + ['/download/curlmanager-1.0.6-x64.exe'] * 8 + ['/download/Element_v2.8.0_UserManual_RevA.pdf'] * 8
    N_FILES = len(file_list)
    MAX_PROCESSES = 50 # depends on your configuration on how well the I/O can be overlapped
    N_PROCESSES = min(N_FILES, MAX_PROCESSES) # no point in creating more threds than required

    with ProcessPoolExecutor(N_PROCESSES) as process_pool_executor:
        results = list(process_pool_executor.map(io_bound_function, file_list))
        print(results)

if __name__ == '__main__':
    print(timeit.timeit(stmt='main()', number=1, globals=globals()))