跳至主要内容

Python 多进程:线程与进程指南

学习如何使用 Python 的 multiprocessing 模块管理线程与进程。了解并行编程关键技巧。通过示例提升代码效率。
· 7分钟 阅读

使用 AI 探索

Python 的标准库内置了多个软件包,让开发者可以立即获得这门语言的诸多便利。

其中一个就是 multiprocessing 模块,它使系统能够同时运行多个进程。换句话说,开发者可以将应用程序拆分为可独立于其 Python 代码运行的更小的线程。这些线程或进程随后由操作系统分配到处理器上,从而并行运行,提升您的 Python 程序的性能与效率。

如果您对线程、进程、处理器等词汇还不熟悉,也不用担心。本文将介绍什么是进程、它与线程有何不同,以及如何使用 multiprocessing 模块。

什么是 Python 中的进程?

理解进程与线程的概念,对于更好地把握操作系统在各个执行阶段如何管理程序非常有帮助。进程本质上是对一个计算机程序的引用。每个程序都对应一个进程。

在您阅读这篇文章时,即使只打开了少数几个程序,您的计算机也很可能同时运行着许多进程——这是因为大多数操作系统都会在后台运行多个任务。比如,您当前也许只运行了三个程序,但计算机可能同时有超过 30 个活动进程在运行。

如何查看计算机上当前的活动进程取决于您的操作系统:

  • Windows:按 Ctrl+Shift+Esc 打开任务管理器
  • Mac:打开 Spotlight 搜索,输入 "Activity Monitor"(活动监视器),然后按下回车
  • Linux:点击应用菜单,搜索 System Monitor(系统监视器)。

Python 可以访问真实的系统级进程。通过从 multiprocessing 模块实例化 Process 类的实例,开发者可以使用 Python 引用底层的原生进程。当一个进程启动时,会在幕后创建一个新的原生进程。一个 Python 进程的生命周期包含三个阶段:新建进程、运行中的进程和已终止的进程——我们将逐一介绍。

所有进程都由一个或多个线程构成。还记得我们提到过“进程只是对计算机程序的引用”吗?每个 Python 程序就是一个包含一个默认线程(称为主线程)的进程。主线程负责执行您 Python 程序中的指令。不过,需要注意的是,进程与线程并不相同。

多进程 vs. 多线程

更具体地说,一个 Python 解释器实例——即将 Python 代码转换为计算机可理解语言的工具——等同于一个进程。一个进程至少包含一个线程,在 Python 中称为“主线程”,当然也可以在同一进程内创建其他线程——在一个进程中创建的所有其他线程都归属于该进程。

线程用于表示您的 Python 程序将如何被执行,一旦所有非后台线程都结束,Python 进程也会终止。

  • 进程(Process):一个进程是一个 Python 解释器实例,至少包含一个称为主线程的线程。
  • 线程(Thread):表示 Python 程序在某个 Python 进程内的执行方式。

Python 提供了两个非常相似的类,使我们能够更好地控制进程与线程:multiprocessing.Process 和 threading.Thread。

下面回顾它们的一些相似点与不同点。

相似点

并发

并发是一种概念,指程序的不同部分可以以乱序或部分有序的方式执行,而不影响最终结果。这两个类最初就是为了并发而设计的。

支持并发原语

multiprocessing.Process 和 threading.Thread 类支持相同的并发原语——这是一类用于同步和协调线程与进程的工具。

统一的 API

一旦您掌握了 multiprocessing.Process 的 API,便可将该知识迁移到 threading.Thread 的 API,反之亦然。它们的设计本就有意保持一致。

不同点

功能

尽管它们的 API 相同,但进程与线程并不相同。进程是比线程更高层的抽象:进程是对一个计算机程序的引用,而线程隶属于进程。这种差异体现在类本身上。因此,这两个类代表了由底层操作系统管理的两种不同的原生功能。

对共享状态的访问

这两个类访问共享状态的方式不同。由于线程隶属于某个进程,它们可以在进程内共享内存。因此,在新线程中执行的函数仍然可以访问进程内相同的数据与状态。线程之间共享状态的方式称为“共享内存”,实现起来相对直接。相反,进程之间共享状态则要复杂得多:状态必须被序列化并在进程之间传输。换句话说,进程不通过共享内存共享状态,因为它们拥有各自独立的内存。取而代之,进程间的共享通过一种称为“进程间通信”(inter-process communication)的技术实现,而在 Python 中执行它需要使用诸如 multiprocessing.Pipe 或 multiprocessing.Queue 等显式工具。

GIL

Python 的全局解释器锁(GIL)是一种锁,它只允许一个线程持有对 Python 解释器的控制权。多个线程受制于 GIL,这常常使得在 CPython 解释器上使用 Python 进行多线程以实现真正的多核执行变得不切实际:Python 并不支持通过多线程实现真正的多核并行。不过,进程不受 GIL 的限制,因为 GIL 作用于每个 Python 进程内部,而不是跨进程。

在使用场景方面,当程序为 CPU 密集型且不需要进行 IO 或用户交互时,多进程往往优于多线程。对于 IO 或网络受限的程序,以及需要提高应用响应性的场景,多线程则更合适。

Python 多进程的优势

不妨把处理器想象成一位企业家。随着企业家的业务增长,需要处理的任务越来越多。如果这位企业家决定独自承担所有任务(如会计、销售、市场、创新等),那么由于一个人在任意时刻只能做有限的事情,她可能会拖累企业的整体效率和表现。比如,在处理创新任务前,她必须先暂停销售任务——这被称为“顺序”执行任务。

大多数企业家都明白,试图一个人包揽一切并不是好主意。因此,他们通常会通过雇佣员工来管理不同部门,以分担不断增长的任务量。这样,任务就可以并行完成——也就是说,不必停止一个任务才能运行另一个任务。雇佣更多员工执行特定任务,就好比使用多个处理器来执行操作。举例来说,计算机视觉项目通常要求处理大量图像数据,这非常耗时;为了加速这一过程,您可以并行处理多张图片。

因此,我们可以说,多进程有助于通过将任务划分并分配给不同处理器,使程序更高效。Python 的 multiprocessing 模块进一步简化了这一点,它作为一个高层工具,通过将任务分配给不同进程来提升程序效率。

Python 的 Multiprocessing 模块基础

我们提到过,Python 进程的生命周期包含三个阶段:新建进程、运行中的进程和已终止的进程。本节将更深入地介绍生命周期的每个阶段,并提供代码示例。您可以访问我们的 DataLab 工作簿跟进学习。

新建进程

新建进程可以理解为通过实例化 Process 类而创建的进程。当我们将 Process 对象赋给一个变量时,就会派生出一个子进程。

from multiprocessing import Process

# Create a new process
process = Process()

此时,我们的进程实例还没有做任何事情,因为我们初始化了一个空的 Process 对象。我们可以通过向类的 target 参数传入要在不同进程中运行的函数,来修改 Process 对象的配置以执行特定函数。

# Create a new process with a specified function to execute.
def example_function():
    pass

new_process = Process(target=example_function)

如果目标函数还有参数,我们只需将它们作为元组传给 Process 对象的 args 参数即可。

提示:通过交互式课程 Writing Functions in Python 学习如何在 Python 中编写函数。

# Create a new process with specific function to execute with args.
def example(args):
    pass

process = Process(target=example, args=("Hi",))

请注意,我们只是创建了一个新进程,但它尚未运行。

下面看看如何运行新进程。

运行中的进程

运行新进程非常简单:只需调用进程实例的 start() 方法。

# Run the new process
process.start()

该操作会通过调用 Process 实例的 run() 方法来启动进程活动。此外,如果在 Process 实例的 target 参数中指定了自定义函数,run() 方法也会负责调用它。

回忆一下我们在教程前面提到的:每个进程至少有一个称为主线程的线程——这是默认设置。因此,当一个子进程启动时,会为该子进程创建并启动主线程。主线程负责执行我们在子进程中的所有代码。

在从 start() 方法返回到子进程终止这段时间内,我们可以通过对 Process 实例调用 is_alive() 方法来检查该进程实例是否存活。

注意:如果您在笔记本环境中测试,is_alive() 的调用必须与对 start() 方法的调用在同一个单元格中,才能捕捉到正在运行的进程。

# Run the new process
process.start()

# Check process is running
process.is_alive()

"""
True
"""

如果进程未在运行,is_alive() 方法的调用将返回 False。

已终止的进程

当 run() 函数返回或退出时,进程即被终止:通常我们不需要显式执行其他操作。换言之,当您设置为目标函数的指令执行完毕后,可以预期该进程会被终止。因此,is_alive() 方法将返回 false。

# Check process is terminated - should return false.
process.is_alive()

"""
False
"""

不过,如果进程遇到未处理的异常或抛出了错误,也会被终止。比如,如果您在目标函数中存在错误,进程就会终止。

# Create a new process with a specific function that has an error
# to execute with args.
def example(args):
    split_args = list(args.split())
    # "name" variable is not in the function namespace - should raise error
    return name

# New process
process = Process(target=example, args=("Hi",))

# Running the new process
process.start()

# Check process is running
process.is_alive()

"""
True

Process Process-15:
Traceback (most recent call last):
  File "/usr/lib/python3.8/multiprocessing/process.py", line 315, in _bootstrap
    self.run()
"""

Process 对象还提供了 terminate() 和 kill() 方法,使用户可以强制终止进程。

# Create a new process with a specific function to execute with args.
def example(args):
    split_args = list(args.split())
    # "name" variable is not in the function namespace - should raise error
    return split_args

# New process
process = Process(target=example, args=("Hi",))

# Running the new process
process.start()

if process.is_alive():
    process.terminate() # You can also use process.kill()
    print("Process terminated forcefully")

"""
Process terminated forcefully
"""

需要注意的是,强制终止并不是推荐的做法:它可能无法安全关闭所有已打开的资源,或无法保存所需的程序状态。更好的方法是使用受控的关闭方式,例如使用对进程安全的布尔标记或类似工具。

Python 多进程教程

既然您已经理解了多进程的基础知识,我们来做一个示例,演示如何在 Python 中进行并发编程。

我们将创建的函数只会打印一条语句、休眠 1 秒,然后再打印一条语句——想进一步了解函数,请参阅这篇 Python 函数教程。

import time

def do_something():
    print("I'm going to sleep")
    time.sleep(1)
    print("I'm awake") 

第一步是创建一个新进程:我们将创建两个。

# Create new child process

process_1 = Process(target=do_something)
process_2 = Process(target=do_something)

在笔记本环境中,并发程序看起来如下:

%%time

# Starts both processes
process_1.start()
process_2.start()

"""
I'm going to sleep
CPU times: user 810 µs, sys: 7.34 ms, total: 8.15 ms
Wall time: 6.04 ms
I'm going to sleep
"""

从程序输出可以明显看出,我们的代码某处存在问题。计时信息在第一个进程的中途被打印出来,而且第二条打印语句没有被打印。

这是因为实际上有三个进程在运行:主进程,以及 process_1 和 process_2。负责统计时间并打印的进程是主进程。若要让主进程在打印时间前先等待,我们必须在运行完两个进程后,对它们调用 join() 方法。

注意:如果您想了解更多,请查看这篇 Stackoverflow 讨论。

来看更新后的代码片段:

%%time
​
# Create new child process (Cannot run a process more than once)
new_process_1 = Process(target=do_something)
new_process_2 = Process(target=do_something)
​
# Starts both processes
new_process_1.start()
new_process_2.start()
​
new_process_1.join()
new_process_2.join()

"""
I'm going to sleep
I'm going to sleep
I'm awake
I'm awake
CPU times: user 0 ns, sys: 14 ms, total: 14 ms
Wall time: 1.01 s
"""

问题解决。

本次运行的 Wall time 比第一次稍长。然而,这次运行在返回时间信息之前,已完成对两个目标函数的调用。我们也可以据此思路,让更多进程并发运行。

关于 Python 多进程的最后思考

在本教程中,您学习了如何通过并发运行让 Python 程序更高效。具体而言,您学到了:

  • 什么是进程,以及如何在您的计算机上查看它们。
  • Python 的 multiprocessing 与 threading 模块之间的相似点与差异。
  • multiprocessing 模块的基础,以及如何使用多进程并发运行 Python 程序。

想要进一步学习 Python 编程?请查看我们的 Python Programmer 职业路径。无需任何编程基础,路径结束时,您将掌握用 Python 开发软件、整理数据并进行高级分析所需的技能。

Kurtis Pykes

作者

Kurtis Pykes

主题