Logo
Section 1 / 2

Linux 上的线程与进程

学习如何在 Python 中创建多线程进程。

导入

Linux 上的线程与进程

学习如何在 Python 中创建多线程进程。

在本课程中,我们将重点介绍如何在 Linux 系统上创建多线程程序,因为 Python 与 Linux 开发密切相关。虽然 Windows 上的理论原理相同,但底层实现方式有所不同。

进程

在 Linux 中运行的任何程序都是一个进程。每个进程都有自己的内存空间、栈以及对共享外设的访问权限。从每个进程的角度来看,它似乎独占整个计算机资源。此外,如果你有 Windows 编程经验(即使用 Microsoft Windows API),会发现术语上存在一些差异。微软为其进程使用了一些特殊术语。

那么多个进程是如何同时运行的呢?这正是操作系统的职责。操作系统确保每个进程只能访问自己的内存、使用 CPU、访问网络等外部设备。由于每个进程都自带完整的执行环境,因此它们被认为是“重量级”的。为了解决这个问题,程序员有时会采用线程。

线程

线程是一种轻量级的进程。它是正在运行的进程中的更小单位,意味着它们共享启动它们的进程的内存和其他资源。如果有一个复杂的算法,我们可以将其拆分为多个线程并行执行。由于所有线程都能访问共享内存,它们可以协同工作而无需复杂的通信机制。这很棒,对吧!

然而,线程容易引发混乱,并可能导致严重的错误。这些错误可能由多种原因引起。例如,可能是时间顺序问题(线程 A 是否在线程 B 之前运行?),也可能是 竞态条件,即两个线程“争抢”更新同一段代码或内存区域。这会导致不可预测的行为。为此,人们发明了锁、信号量等机制来解决这些问题。深入探讨这些内容已超出本课程范围。

Python 本身并不原生支持线程,而是依赖于操作系统的线程机制(在 Linux 上是 Pthreads,在 Windows 上是 Windows 线程)。

但假设我们遇到了一个理想的问题:完全独立,不涉及数据共享,因此不存在竞态条件或时序问题的风险。这种情况在复杂的数学计算中是可能存在的。在这种情况下,应始终使用线程。

...
Single thread vs. multithreaded process

线程与单线程执行对比

让我们验证一下这个理论。

1import random 2 3def gen_random_data(): 4 arr = [] 5 for i in range(100000): 6 arr.append(random.random()) 7 print("job done")

我们定义了一个名为 gen_random_data 的简单函数,用于生成随机数据。我们在下面代码块的第 3 行导入该文件,以模拟纯粹的数学问题。接下来,我们将在这段代码的第 7 到第 10 行中运行此函数四次。

通过多次调用,我们可以观察到使用线程的效果(每次调用都会分配一个独立的线程)。

1#!/usr/bin/python 2import time 3from gen_rand import gen_random_data 4 5if __name__ == '__main__': 6 gen_random_data() 7 gen_random_data() 8 gen_random_data() 9 gen_random_data() 10 11 print(time.process_time(), "seconds")

我们使用 Python 的 time 模块来测量代码运行时间。结果可能因操作系统和处理器的不同而有所差异,但整体趋势应保持一致。我们运行了三次代码并取最常见的值。

现在,我们尝试使用线程。

这次我们导入了 threading 模块。

1#!/usr/bin/python 2import threading 3import time 4from time import process_time 5from gen_rand import gen_random_data 6 7if __name__ == '__main__': 8 for x in range(4): 9 process = threading.Thread(target=gen_random_data) 10 process.start()

threading.Thread 函数用于创建一个线程,target 参数指定要执行的函数名称,在这里就是 gen_random_data。我们创建一个线程并用 process.start() 启动它。整个过程非常直观。既然线程是并行运行的,应该更快才对,对吧?

让我们看看这些线程的执行时间。令人意外的是,某些线程的执行时间甚至比单线程版本还长。

这怎么可能?为什么使用线程反而让代码变慢了?

欢迎来到 全局解释器锁(GIL)。GIL 是 Python 的一个“特性”,它阻止了线程真正并行运行。GIL 会强制各个线程轮流获取 CPU 访问权限,这意味着实际上相当于单线程执行,只是多了创建和管理线程的开销。

为了解决这个问题,Multiprocessing 模块被引入。它的接口与 threading 模块非常相似,因此从线程切换到多进程变得相对容易。

下面我们来看看使用 multiprocessing 的版本。

1#!/usr/bin/python 2import multiprocessing 3from gen_rand import gen_random_data 4 5if __name__ == '__main__': 6 for x in range(4): 7 process = multiprocessing.Process(target=gen_random_data) 8 process.start()

可以看到,代码结构与 threading 版本非常相似,只是将模块换成了 multiprocessing。

这一次,执行时间几乎缩短到了单线程版本的一半。主要原因在于创建进程存在一定的开销。其他影响因素还包括后台运行的其他进程数量,以及磁盘或网络访问的需求程度。

注意:多进程并非总是最佳选择!

多进程的一个问题是缺乏共享状态。如果我们需要在进程之间传递数据,就必须自行设计解决方案,或者借助操作系统提供的机制,比如 Linux 中的套接字。

编写多线程或多进程代码很快就会变得复杂难控。对于小型问题或许有用,但在生产环境中并不推荐。

在 Python 中创建一个词频统计器

测验:多线程与多进程