Python 之开启多进程(multiprocessing.Process)的基本使用以及原理(76)

#王者杯·14天创作挑战营·第1期#

Python 之开启多进程(multiprocessing.Process)的基本使用以及原理

一、引言

在计算机程序运行过程中,单进程执行模式在处理复杂任务时往往存在效率瓶颈。随着硬件多核处理器的普及,多进程编程成为提升程序性能的重要手段。Python 提供了 multiprocessing 模块,其中 multiprocessing.Process 类作为核心工具,用于创建和管理多个进程。通过使用多进程,程序可以充分利用多核 CPU 的资源,将不同任务分配到多个进程并行执行,从而显著提高整体执行效率。本文将深入探讨 multiprocessing.Process 的基本使用方法和背后的原理,通过丰富的代码示例帮助读者掌握这一重要的编程特性。

二、多进程基础概念

2.1 进程的定义

进程是程序在操作系统中的一次执行实例,是操作系统进行资源分配和调度的基本单位。每个进程拥有独立的内存空间、系统资源(如文件描述符、网络连接等)和执行上下文。例如,当打开一个文本编辑器应用程序时,操作系统会为其创建一个进程,该进程负责运行编辑器的代码并管理相关资源。

2.2 多进程的优势

  1. 提高执行效率:在多核 CPU 环境下,多个进程可以同时在不同核心上运行,实现真正的并行计算,缩短任务执行时间。
  2. 资源隔离:每个进程独立运行,一个进程的崩溃不会影响其他进程,增强了程序的稳定性。
  3. 充分利用系统资源:多进程可以同时利用 CPU、内存、磁盘 I/O 等资源,避免单一资源的过度占用。

三、multiprocessing.Process 的基本使用

3.1 简单进程创建与启动

使用 multiprocessing.Process 创建和启动一个进程的基本步骤如下:

import multiprocessing

# 定义一个函数,作为子进程要执行的任务
def worker():
    print(f"子进程 {multiprocessing.current_process().name} 开始执行")
    # 模拟子进程执行任务,这里简单休眠 2 秒
    import time
    time.sleep(2)
    print(f"子进程 {multiprocessing.current_process().name} 执行结束")

if __name__ == '__main__':
    # 创建一个 Process 对象,target 参数指定子进程要执行的函数
    p = multiprocessing.Process(target=worker)
    # 启动子进程
    p.start()
    print(f"主进程 {multiprocessing.current_process().name} 继续执行其他任务")
    # 等待子进程执行完毕
    p.join()
    print("所有子进程已执行完毕,主进程结束")

在上述代码中:

  1. 首先导入 multiprocessing 模块,它提供了多进程相关的功能。
  2. 定义 worker 函数,该函数将作为子进程执行的任务。函数内部打印进程名称,模拟执行任务(通过 time.sleep 休眠 2 秒),最后打印任务结束信息。
  3. if __name__ == '__main__': 代码块中:
    • 使用 multiprocessing.Process 创建进程对象 ptarget 参数指定了子进程要执行的函数 worker
    • 调用 p.start() 方法启动子进程,此时子进程开始独立运行。
    • 主进程继续执行后续代码,打印相关信息。
    • 调用 p.join() 方法,主进程会阻塞等待子进程执行完毕,确保子进程任务完成后再继续执行主进程后续代码。

3.2 传递参数给子进程

可以通过 argskwargs 参数向子进程传递数据:

import multiprocessing

# 定义一个带参数的函数,作为子进程任务
def worker_with_args(name, age):
    print(f"子进程 {multiprocessing.current_process().name} 接收参数:name={name}, age={age}")
    # 模拟子进程执行任务
    import time
    time.sleep(1)
    print(f"子进程 {multiprocessing.current_process().name} 执行结束")

if __name__ == '__main__':
    # 使用 args 参数传递位置参数
    p1 = multiprocessing.Process(target=worker_with_args, args=('Alice', 25))
    # 使用 kwargs 参数传递关键字参数
    p2 = multiprocessing.Process(target=worker_with_args, kwargs={'name': 'Bob', 'age': 30})
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    print("所有子进程已执行完毕,主进程结束")

在这个示例中:

  1. 定义 worker_with_args 函数,该函数接收 nameage 两个参数。
  2. 在创建进程对象时:
    • p1 使用 args 参数以元组形式传递位置参数 ('Alice', 25)
    • p2 使用 kwargs 参数以字典形式传递关键字参数 {'name': 'Bob', 'age': 30}
  3. 启动并等待两个子进程执行完毕。

3.3 多个进程并发执行

创建多个进程并让它们并发执行任务:

import multiprocessing

# 定义子进程执行的任务函数
def task(num):
    print(f"子进程 {multiprocessing.current_process().name} 开始执行任务 {num}")
    import time
    time.sleep(num)
    print(f"子进程 {multiprocessing.current_process().name} 完成任务 {num}")

if __name__ == '__main__':
    processes = []
    for i in range(5):
        # 创建 5 个进程对象
        p = multiprocessing.Process(target=task, args=(i,))
        processes.append(p)
        p.start()
    for p in processes:
        p.join()
    print("所有子进程已执行完毕,主进程结束")

代码说明:

  1. 定义 task 函数,接收一个参数 num,模拟不同耗时的任务。
  2. 通过循环创建 5 个进程对象,每个进程执行 task 函数并传入不同的参数 i
  3. 启动所有进程后,使用 join 方法依次等待每个进程执行完毕。

四、multiprocessing.Process 的原理

4.1 操作系统层面的进程创建

Python 的 multiprocessing.Process 本质上依赖操作系统提供的进程创建机制:

  • Unix/Linux 系统:使用 fork 系统调用。fork 会创建一个与父进程完全相同的子进程,子进程继承父进程的内存空间、文件描述符等资源。在 fork 调用返回后,父进程和子进程通过返回值区分(父进程返回子进程的 PID,子进程返回 0)。
  • Windows 系统:使用 CreateProcess 系统调用。该调用会加载一个新的可执行文件,创建全新的进程环境,不涉及内存空间的直接复制,而是重新初始化资源。

4.2 Python 中的实现细节

  1. multiprocessing.Process 类的初始化
class Process(object):
    def __init__(self, group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None):
        self._target = target  # 子进程要执行的函数
        self._args = args  # 传递给函数的位置参数
        self._kwargs = kwargs  # 传递给函数的关键字参数
        self._name = name  # 进程名称
        self._daemon = daemon  # 是否为守护进程

在创建 Process 对象时,会保存目标函数、参数、进程名称等信息。

  1. 进程启动与执行
    当调用 start 方法时,Process 类会通过 _Popen 类(在不同操作系统下有不同实现)来启动子进程:
def start(self):
    '''Start child process'''
    self._popen = self._Popen(self)  # 创建进程对象并启动
    self._sentinel = self._popen.sentinel  # 获取进程的标识

在 Unix/Linux 系统中,_Popen 类会调用 fork 并在子进程中执行目标函数;在 Windows 系统中,_Popen 类会使用 CreateProcess 启动新进程,并通过管道等方式传递必要的参数和信息。

  1. 进程间的资源隔离
    每个 Process 对象创建的子进程都有独立的内存空间,这意味着不同进程之间的变量是相互隔离的,不会相互影响。例如:
import multiprocessing

count = 0  # 主进程中的变量

def increment():
    global count
    for _ in range(1000000):
        count += 1
    print(f"子进程中 count 的值: {count}")

if __name__ == '__main__':
    p = multiprocessing.Process(target=increment)
    p.start()
    p.join()
    print(f"主进程中 count 的值: {count}")

运行结果显示,主进程和子进程中的 count 变量是独立的,子进程对 count 的修改不会影响主进程中的 count 值。

4.3 进程的生命周期管理

  1. 创建状态:调用 Process 类的构造函数创建进程对象时,进程处于创建状态,此时仅初始化了进程的基本信息。
  2. 就绪状态:调用 start 方法后,进程进入就绪状态,等待操作系统分配 CPU 资源。
  3. 运行状态:当操作系统调度到该进程时,进程进入运行状态,开始执行目标函数。
  4. 阻塞状态:如果进程执行 I/O 操作(如文件读写、网络请求)或等待其他资源,会进入阻塞状态,让出 CPU 资源。
  5. 终止状态:当目标函数执行完毕或通过 terminate 方法强制结束进程时,进程进入终止状态,操作系统回收其占用的资源。

join 方法用于等待进程结束,其内部通过操作系统提供的等待机制(如 Unix 中的 wait 系统调用)来实现阻塞等待。

五、多进程编程的注意事项

5.1 if __name__ == '__main__': 的必要性

在 Windows 和 macOS 系统中,multiprocessing 模块默认采用 spawn 方式创建子进程。这种方式下,子进程会重新导入主模块的代码。如果不使用 if __name__ == '__main__': 语句块包裹创建进程的代码,会导致子进程不断递归创建新的进程,最终引发错误。例如:

import multiprocessing

def infinite_create():
    p = multiprocessing.Process(target=infinite_create)
    p.start()
    p.join()

# 错误示例,不使用 if __name__ == '__main__' 会导致无限递归创建进程
# infinite_create()

if __name__ == '__main__':
    # 正确示例
    p = multiprocessing.Process(target=infinite_create)
    p.start()
    p.join()

5.2 进程间的资源竞争与同步

虽然进程间内存空间相互隔离,但在访问共享资源(如文件、数据库)时仍可能出现竞争问题。例如多个进程同时写入同一个文件,可能导致数据混乱。此时需要使用同步机制(如 multiprocessing.Lock)来解决:

import multiprocessing

def write_to_file(lock):
    with lock:
        with open('test.txt', 'a') as f:
            f.write(f"进程 {multiprocessing.current_process().name} 写入内容\n")

if __name__ == '__main__':
    lock = multiprocessing.Lock()
    processes = []
    for _ in range(5):
        p = multiprocessing.Process(target=write_to_file, args=(lock,))
        processes.append(p)
        p.start()
    for p in processes:
        p.join()

上述代码通过 Lock 对象确保同一时间只有一个进程能够写入文件。

5.3 守护进程的使用

可以通过设置 daemon 参数将进程设置为守护进程。守护进程会在主进程结束时自动终止:

import multiprocessing
import time

def daemon_task():
    while True:
        print("守护进程在运行...")
        time.sleep(1)

if __name__ == '__main__':
    p = multiprocessing.Process(target=daemon_task, daemon=True)
    p.start()
    time.sleep(3)
    print("主进程结束,守护进程自动终止")

在这个示例中,守护进程会持续运行,但当主进程执行 3 秒后结束时,守护进程会被自动终止。

六、总结与展望

6.1 总结

本文全面介绍了 Python 中使用 multiprocessing.Process 开启多进程的基本使用方法和原理。通过示例代码展示了进程的创建、启动、参数传递以及并发执行的实现方式,深入剖析了其在操作系统层面和 Python 内部的运行机制,并阐述了多进程编程中的常见注意事项。掌握这些知识有助于开发者利用多进程提升程序性能,实现资源的高效利用。

6.2 展望

  1. 性能优化:随着硬件技术的发展,未来 Python 可能进一步优化 multiprocessing 模块的性能,更好地利用多核 CPU 和新型硬件架构。
  2. 简化编程模型:可能会出现更简洁、易用的多进程编程接口,降低开发者编写多进程程序的难度。
  3. 与其他技术结合:多进程与异步编程、分布式计算等技术的融合将成为趋势,为复杂场景提供更强大的解决方案。

通过不断探索和实践,Python 多进程编程将在更多领域发挥重要作用,帮助开发者构建更高效、稳定的应用程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Android 小码蜂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值