Python并发多进程编程

一、多进程(Multiprocessing)

多进程是指在操作系统中同时运行多个进程,每个进程都有自己独立的内存空间和系统资源。Python 的multiprocessing模块提供了多进程编程的支持。

优点

  • 可以利用多核 CPU 的优势,并行执行任务,提高程序的运行效率。
  • 各个进程之间相互独立,一个进程的崩溃不会影响其他进程。

缺点

  • 进程的创建和销毁开销较大。
  • 进程之间的通信和数据共享相对复杂。

代码示例

import multiprocessing

# 定义一个简单的任务函数
def worker(num):
    """进程要执行的任务"""
    # 打印当前工作进程的编号,表示该进程已开始执行任务
    print(f'Worker {num} started')
    # 计算当前工作进程编号的平方,作为任务的结果
    result = num * num
    # 打印当前工作进程的编号和任务结果,表示该进程已完成任务
    print(f'Worker {num} finished with result: {result}')
    # 返回任务结果
    return result

if __name__ == '__main__':
    # 创建一个进程池,最多同时运行3个进程
    pool = multiprocessing.Pool(processes=3)
    # 向进程池提交任务
    results = [pool.apply_async(worker, args=(i,)) for i in range(5)]
    # 关闭进程池,不再接受新的任务
    pool.close()
    # 等待所有进程完成任务
    pool.join()
    # 获取每个进程的返回结果
    output = [p.get() for p in results]
    # 打印最终结果,即所有进程执行任务后的返回结果
    print(f'Final results: {output}')

二、充分利用CPU

multiprocessing.Pool 并不是最多只能同时运行 3 个进程,在创建 Pool 对象时,processes 参数指定了进程池中的进程数量,示例代码中设置为 3 只是一个示例,你可以根据实际需求调整该参数。

如何全面使用 16 核 CPU?

当你的 CPU 是 16 核时,你可以将 processes 参数设置为 16,这样进程池就会创建 16 个进程,理论上可以充分利用 CPU 的多核性能。不过,在实际应用中,还需要考虑系统资源的其他使用情况(如内存),有时候并不一定需要将进程数量设置得和 CPU 核心数完全一致。

下面是一个示例代码,展示了如何将 processes 参数设置为 16 来充分利用 16 核 CPU:

import multiprocessing

# 定义一个简单的任务函数,模拟 CPU 密集型任务
def worker(num):
    """进程要执行的任务"""
    # 打印当前进程的开始信息
    print(f'Worker {num} started')
    # 模拟一些 CPU 密集型计算
    # 初始化结果变量
    result = 0
    # 循环从0到999999
    for i in range(1000000):
        # 将当前循环变量i累加到结果变量result中
        result += i
    # 打印当前进程的完成信息
    print(f'Worker {num} finished')
    # 返回累加结果
    return result

if __name__ == '__main__':
    # 获取 CPU 的核心数
    cpu_count = multiprocessing.cpu_count()
    print(f"CPU 核心数: {cpu_count}")

    # 创建一个进程池,进程数量设置为 CPU 核心数
    pool = multiprocessing.Pool(processes=cpu_count)
    # 向进程池提交任务,这里假设要执行 20 个任务
    results = [pool.apply_async(worker, args=(i,)) for i in range(20)]
    # 关闭进程池,不再接受新的任务
    pool.close()
    # 等待所有进程完成任务
    pool.join()
    # 获取每个进程的返回结果
    output = [p.get() for p in results]
    # 打印最终结果列表的长度
    print(f'Final results length: {len(output)}')

代码解释

  1. multiprocessing.cpu_count():用于获取当前系统的 CPU 核心数。
  2. multiprocessing.Pool(processes=cpu_count):创建一个进程池,进程数量设置为 CPU 核心数,这样可以充分利用多核 CPU 的性能。
  3. pool.apply_async(worker, args=(i,)):向进程池异步提交任务,worker 是任务函数,args 是传递给任务函数的参数。
  4. pool.close():关闭进程池,不再接受新的任务。
  5. pool.join():等待所有进程完成任务。
  6. p.get():获取每个进程的返回结果。

 注意事项

  • 虽然设置进程数量为 CPU 核心数可以充分利用多核性能,但在实际应用中,还需要考虑内存等其他系统资源的使用情况。如果任务需要大量的内存,过多的进程可能会导致内存不足。
  • 对于 I/O 密集型任务,使用多进程可能并不是最佳选择,异步编程或多线程可能更合适。

三、多进程之间的通讯和数据共享

在 Python 多进程编程中,由于每个进程都有自己独立的内存空间,进程间的通讯(Inter - Process Communication,IPC)和数据共享就变得尤为重要。下面说明几种常见的进程间通讯和数据共享的方式:

1. Queue(队列)

multiprocessing.Queue 是一个线程和进程安全的队列,可用于在多个进程间传递数据。

示例代码

import multiprocessing

def producer(queue):
    # 生产者进程,向队列中放入数据
    for i in range(5):
        queue.put(i)
        print(f"Produced {i}")

def consumer(queue):
    # 消费者进程,从队列中取出数据
    while True:
        item = queue.get()
        if item is None:
            break
        print(f"Consumed {item}")

if __name__ == '__main__':
    # 创建一个队列
    queue = multiprocessing.Queue()
    # 创建生产者进程
    p1 = multiprocessing.Process(target=producer, args=(queue,))
    # 创建消费者进程
    p2 = multiprocessing.Process(target=consumer, args=(queue,))

    p1.start()
    p2.start()

    p1.join()
    # 向队列中放入 None 作为结束信号
    queue.put(None)
    p2.join()

    print("All processes finished.")

代码解释

  • multiprocessing.Queue():创建一个队列对象。
  • queue.put(item):将数据 item 放入队列。
  • queue.get():从队列中取出数据。

2. Pipe(管道)

multiprocessing.Pipe 返回一对连接对象,可用于在两个进程间进行双向通讯。

示例代码

import multiprocessing

def sender(conn):
    # 发送进程,向管道发送数据
    messages = ["Hello", "World", "!"]
    for message in messages:
        conn.send(message)
        print(f"Sent: {message}")
    conn.close()

def receiver(conn):
    # 接收进程,从管道接收数据
    while True:
        try:
            message = conn.recv()
            print(f"Received: {message}")
        except EOFError:
            break
    conn.close()

if __name__ == '__main__':
    # 创建一个管道,返回两个连接对象
    parent_conn, child_conn = multiprocessing.Pipe()
    # 创建发送进程
    p1 = multiprocessing.Process(target=sender, args=(child_conn,))
    # 创建接收进程
    p2 = multiprocessing.Process(target=receiver, args=(parent_conn,))

    p1.start()
    p2.start()

    p1.join()
    p2.join()

    print("All processes finished.")

代码解释

  • multiprocessing.Pipe():创建一个管道,返回两个连接对象 parent_conn 和 child_conn
  • conn.send(data):向管道发送数据。
  • conn.recv():从管道接收数据。

3. Value 和 Array

multiprocessing.Value 和 multiprocessing.Array 可用于在多个进程间共享单个值或数组。

示例代码

import multiprocessing

def increment(counter):
    # 增加共享值
    for _ in range(1000):
        with counter.get_lock():
            counter.value += 1

if __name__ == '__main__':
    # 创建一个共享的整数对象
    counter = multiprocessing.Value('i', 0)
    processes = []
    # 创建多个进程
    for _ in range(4):
        p = multiprocessing.Process(target=increment, args=(counter,))
        processes.append(p)
        p.start()

    for p in processes:
        p.join()

    print(f"Final counter value: {counter.value}")

代码解释

  • multiprocessing.Value('i', 0):创建一个共享的整数对象,初始值为 0。'i' 表示整数类型。
  • counter.get_lock():获取锁,确保在修改共享值时不会发生数据竞争。
  • counter.value:访问共享值。

4. Manager

multiprocessing.Manager 提供了一种更高级的方式来实现进程间的数据共享,它可以创建共享的列表、字典等对象。

示例代码

import multiprocessing

def worker(dictionary, key, value):
    # 向共享字典中添加键值对
    dictionary[key] = value

if __name__ == '__main__':
    # 创建一个管理器对象
    manager = multiprocessing.Manager()
    # 创建一个共享的字典
    shared_dict = manager.dict()
    processes = []
    # 创建多个进程
    for i in range(5):
        p = multiprocessing.Process(target=worker, args=(shared_dict, i, i * 2))
        processes.append(p)
        p.start()

    for p in processes:
        p.join()

    print(f"Shared dictionary: {shared_dict}")

代码解释

  • multiprocessing.Manager():创建一个管理器对象。
  • manager.dict():创建一个共享的字典对象。
  • 可以像操作普通字典一样操作共享字典。

通过上述四种方式,你可以在 Python 多进程编程中实现进程间的通讯和数据共享。不同的方式适用于不同的场景,你可以根据具体需求选择合适的方法。


四、Python多进程方式的独特优势

1. 充分利用多核 CPU

在现代计算机系统中,CPU 通常具有多个核心。然而,由于 Python 的全局解释器锁(GIL)的存在,多线程在处理 CPU 密集型任务时并不能充分发挥多核 CPU 的性能优势。而 multiprocessing 模块可以创建多个独立的进程,每个进程都有自己独立的 Python 解释器实例和内存空间,它们可以并行地在不同的 CPU 核心上运行,从而充分利用多核 CPU 的计算能力,显著提高程序在 CPU 密集型任务上的执行效率。

2. 提高程序的稳定性

由于每个进程都是相互独立的,一个进程的崩溃不会影响其他进程的正常运行。如果在多线程编程中,某个线程出现了未处理的异常,可能会导致整个进程崩溃,从而影响其他线程的执行。而在多进程编程中,各个进程之间相互隔离,一个进程的异常只会导致该进程终止,其他进程可以继续正常工作,从而提高了程序的整体稳定性。

3. 数据安全性高

每个进程都有自己独立的内存空间,进程之间的数据是相互隔离的。这意味着在一个进程中对数据的修改不会影响其他进程中的数据,避免了多线程编程中常见的数据竞争和并发访问问题,提高了数据的安全性。当需要对数据进行并发处理时,只需将数据复制到各个进程中进行处理,处理完成后再进行汇总即可。

4. 适合处理复杂任务

multiprocessing 模块提供了丰富的进程管理和通信机制,如 Process 类用于创建和管理进程,QueuePipe 用于进程间通信,ValueArray 和 Manager 用于进程间数据共享等。这些功能使得 multiprocessing 非常适合处理复杂的任务,例如分布式计算、大规模数据处理等。可以将一个复杂的任务分解为多个子任务,每个子任务由一个独立的进程来处理,通过进程间的通信和协作完成整个任务。

5. 易于使用和集成

multiprocessing 模块的 API 设计简洁明了,易于学习和使用。它与 Python 的其他标准库和第三方库兼容性良好,可以方便地集成到现有的 Python 项目中。无论是小型脚本还是大型应用程序,都可以很容易地使用 multiprocessing 模块来实现多进程编程,提高程序的性能和并发处理能力。


 五、多进程(multiprocessing)的最佳用途 

multiprocessing 是 Python 标准库中用于实现多进程编程的模块,它在以下几种场景中能发挥最佳作用:

1. CPU 密集型任务

由于 Python 的全局解释器锁(GIL),多线程在处理 CPU 密集型任务时无法充分利用多核 CPU 的优势。而 multiprocessing 可以创建多个独立的进程,每个进程都有自己独立的 Python 解释器实例,能并行运行在不同的 CPU 核心上,显著提升计算效率。

适用场景举例

  • 科学计算:如进行大规模的矩阵运算、数值模拟等。以矩阵乘法为例,对于大规模矩阵,计算量巨大,使用多进程可以将矩阵分割成多个子矩阵,每个进程负责计算一部分子矩阵的乘积,最后再合并结果。
  • 图像和视频处理:像图像的滤镜处理、视频的编码解码等操作。例如在图像的卷积操作中,可以将图像分成多个区域,每个进程处理一个区域的卷积计算。

代码示例

import multiprocessing

def cpu_intensive_task(num):
    result = 0
    for i in range(1000000):
        result += i
    return result

if __name__ == '__main__':
    pool = multiprocessing.Pool(processes=multiprocessing.cpu_count())
    tasks = [i for i in range(10)]
    results = pool.map(cpu_intensive_task, tasks)
    pool.close()
    pool.join()
    print("All CPU - intensive tasks are done.")

2. 并行处理大量独立任务

当有大量相互独立的任务需要处理时,使用 multiprocessing 可以并行执行这些任务,从而大大缩短整体处理时间。

适用场景举例

  • 数据批量处理:例如对大量文件进行数据分析、转换或清洗。可以将文件列表分成多个部分,每个进程负责处理一部分文件。
  • 网页爬虫:在爬取多个网站或大量网页时,每个网页的爬取任务可以看作是独立的,使用多进程可以同时发起多个爬取请求,提高爬取效率。

代码示例

import multiprocessing

def process_file(file_path):
    # 模拟文件处理操作
    print(f"Processing {file_path}")
    return file_path

if __name__ == '__main__':
    file_paths = [f"file_{i}.txt" for i in range(20)]
    pool = multiprocessing.Pool(processes=4)
    results = pool.map(process_file, file_paths)
    pool.close()
    pool.join()
    print("All file processing tasks are done.")

3. 提高程序的稳定性和可靠性

由于每个进程都是相互独立的,一个进程的崩溃不会影响其他进程的正常运行。因此,在对程序稳定性要求较高的场景中,multiprocessing 是一个很好的选择。

适用场景举例

  • 服务器端应用:例如 Web 服务器、数据库服务器等。当处理多个客户端请求时,每个请求可以由一个独立的进程来处理,这样即使某个请求处理过程中出现异常,也不会影响其他请求的处理。
  • 长时间运行的后台任务:如监控系统、定时任务等。使用多进程可以确保某个任务的失败不会导致整个系统崩溃。

4. 复杂任务的并行分解

对于一些复杂的任务,可以将其分解为多个子任务,每个子任务由一个独立的进程来处理,通过进程间的通信和协作完成整个任务。

适用场景举例

  • 分布式计算:在分布式系统中,将一个大的计算任务分解成多个小任务,分配给不同的进程或节点进行计算,最后汇总结果。
  • 人工智能训练:在深度学习模型训练中,可以将数据分割成多个批次,每个进程负责训练一个批次的数据,加速训练过程。

代码示例(简单的任务分解)

import multiprocessing

def subtask(task_id, result_queue):
    # 模拟子任务处理
    result = task_id * 2
    result_queue.put(result)

if __name__ == '__main__':
    result_queue = multiprocessing.Queue()
    processes = []
    for i in range(5):
        p = multiprocessing.Process(target=subtask, args=(i, result_queue))
        processes.append(p)
        p.start()

    for p in processes:
        p.join()

    final_results = []
    while not result_queue.empty():
        final_results.append(result_queue.get())
    print("Final results:", final_results)

综上所述,multiprocessing 在 CPU 密集型任务、并行处理大量独立任务、提高程序稳定性以及复杂任务的并行分解等方面具有显著优势,是 Python 中实现高效并发编程的重要工具。


 六、多进程(multiprocessing)最不适宜的用途

multiprocessing 模块虽然在很多场景下非常有用,但也存在一些不太适宜使用的情况,以下是具体介绍:

1. 简单的 I/O 密集型任务

对于简单的 I/O 密集型任务,如少量的文件读写、简单的网络请求等,使用 multiprocessing 并不是一个好的选择,原因如下:

  • 进程创建和销毁开销大:创建新进程需要分配系统资源,包括内存和 CPU 时间等。在简单的 I/O 密集型任务中,进程创建和销毁的开销可能会远远超过任务本身的执行时间,导致性能下降。
  • 上下文切换开销:进程之间的上下文切换也需要消耗系统资源。对于简单的 I/O 操作,频繁的进程切换会增加额外的开销,降低程序的整体效率。

示例场景:一个脚本需要依次读取几个小文件的内容,使用单线程或多线程通常会比多进程更高效。

代码示例(不适合用多进程的情况):

import multiprocessing

def read_file(file_path):
    with open(file_path, 'r') as f:
        content = f.read()
    return content

if __name__ == '__main__':
    file_paths = ['file1.txt', 'file2.txt', 'file3.txt']
    # 这里使用多进程处理简单文件读取,效率不高
    pool = multiprocessing.Pool(processes=len(file_paths))
    results = pool.map(read_file, file_paths)
    pool.close()
    pool.join()
    print(results)

更好的做法是使用单线程:

def read_file(file_path):
    with open(file_path, 'r') as f:
        content = f.read()
    return content

file_paths = ['file1.txt', 'file2.txt', 'file3.txt']
results = []
for file_path in file_paths:
    results.append(read_file(file_path))
print(results)

2. 资源受限的环境

在资源受限的环境中,如内存有限的嵌入式设备或运行在资源紧张的服务器上,使用 multiprocessing 可能会导致系统资源耗尽,出现以下问题:

  • 内存占用高:每个进程都有自己独立的内存空间,创建多个进程会占用大量的内存。如果系统内存不足,可能会导致程序崩溃或系统性能急剧下降。
  • CPU 资源竞争:过多的进程会竞争 CPU 资源,导致系统负载过高,影响其他程序的正常运行。

3. 任务间高度依赖和频繁通信的场景

当任务之间存在高度依赖关系,需要频繁进行数据交换和同步时,multiprocessing 可能不是最佳选择,因为:

  • 进程间通信成本高:进程间通信(IPC)需要通过特定的机制,如队列、管道等,这些机制的实现会带来一定的开销。频繁的 IPC 操作会降低程序的性能。
  • 同步难度大:为了保证数据的一致性和正确性,需要进行复杂的同步操作,如加锁等。这会增加代码的复杂度,并且可能引入死锁等问题。

示例场景:一个任务需要等待另一个任务的中间结果才能继续执行,并且这种交互非常频繁,使用多线程或异步编程可能更合适。

4. 代码复杂度要求低的简单脚本

对于一些简单的脚本,开发者可能更注重代码的简洁性和易维护性。使用 multiprocessing 会引入进程管理、进程间通信等复杂的概念,增加代码的复杂度。在这种情况下,单线程或简单的多线程实现可能更符合需求。

示例:一个简单的脚本用于计算一组数字的总和,使用单线程代码会更简洁易懂:

numbers = [1, 2, 3, 4, 5]
total = sum(numbers)
print(total)

而使用多进程来实现会使代码变得复杂,且没有明显的性能提升。

七、笔者(老司机)的告诫

 1、资源管理方面

  • 合理分配进程数:别盲目多开进程,依任务类型和系统核心数设置,CPU 密集型接近物理核心数,I/O 密集型可多些,否则会因创建、切换开销大而拖慢性能。
  • 关注内存占用:每个进程有独立内存空间,多进程会大量占内存,资源受限环境易致系统崩溃,要做好内存规划。

2、代码编写方面

  • 遵守启动方法规则:不同操作系统启动进程方法有别,Windows 和 macOS 默认 spawn,Linux 默认 fork,确保代码在不同系统兼容。
  • 小心全局变量:各进程有独立内存,全局变量不共享,修改全局变量不会影响其他进程,别依赖全局变量通信。

3、进程间协作方面

  • 通信要高效:进程通信靠队列、管道等,频繁通信开销大,尽量减少通信次数和数据量。
  • 同步要谨慎:涉及共享资源访问,用锁机制同步,操作不当会引发死锁,加锁范围要最小化(能不用,就不用)。

4、调试和维护方面

  • 调试有难度:多进程调试复杂,一个进程崩溃不影响其他,可多打日志,用单进程先调试逻辑。
  • 代码可维护性:多进程增加代码复杂度,合理划分功能模块,写好注释,提高代码可读性和可维护性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值