探索 Python 中的多线程
在本指南中,您将深入了解 Python 多线程的功能,利用其内置的 threading
模块。
我们将从进程和线程的基础概念开始,阐明 Python 中多线程的工作原理,同时解释并发和并行的概念。 随后,我们将学习如何使用 threading
模块在 Python 中启动和管理多个线程。
让我们开始探索吧!
进程 vs 线程:区别是什么?
什么是进程?
进程指的是一个正在运行的程序的实例。
它可以是任何事物,比如 Python 脚本、网页浏览器(如 Chrome)或视频会议应用。 您可以通过打开计算机的任务管理器,查看“性能”选项卡下的 CPU 部分,来查看 CPU 内核上当前运行的进程和线程。
理解进程和线程
在计算机内部,每个进程都拥有独立的内存空间,用于存储其代码和数据。
一个进程可以包含一个或多个线程。 线程是操作系统能够执行的最小指令序列,代表了执行的路径。
每个线程都有自己的堆栈和寄存器,但没有独立的内存空间。 同一个进程内的所有线程可以访问相同的数据。 因此,数据和内存都是由该进程的所有线程共享的。
在具有 N 个内核的 CPU 上,最多可以并行执行 N 个进程。 然而,同一个进程的两个线程永远无法并行执行,但是它们可以并发执行。 我们将在后续章节中详细讨论并发与并行的概念。
根据以上内容,让我们总结一下进程和线程之间的差异。
特性 | 进程 | 线程 |
内存 | 独立内存空间 | 共享内存空间 |
执行方式 | 并行、并发 | 并发;但不是由操作系统直接并行执行,而是由 CPython 解释器处理 |
Python 中的多线程
在 Python 中,全局解释器锁 (GIL) 确保在任何给定时间点只有一个线程可以获得锁并执行。 所有线程都需要获取这个锁才能运行,这保证了在同一时刻只有一个线程在执行,从而避免了多线程同时运行带来的问题。
例如,假设同一进程中有两个线程 t1 和 t2。 当线程 t1 读取某个变量 k 的值时,线程 t2 可能会修改 k 的值。这可能会导致死锁和不正确的结果。 但是,由于每次只有一个线程可以获得锁并执行,GIL 保证了线程安全。
那么,我们如何在 Python 中实现多线程呢? 要理解这一点,我们需要先了解并发和并行的概念。
并发与并行:概述
假设我们有一个多核 CPU。 在下面的图中,CPU 有四个内核,这意味着我们可以在任何给定时刻同时并行执行四个不同的操作。
如果有四个进程,那么每个进程都可以在四个内核上独立地并行运行。 如果每个进程又有两个线程呢?
为了理解线程的工作方式,让我们从多核处理器切换到单核处理器。 如前所述,在特定的执行时刻,只能有一个线程处于活动状态。 但处理器内核可以在线程之间切换。
例如,I/O 密集型线程常常需要等待 I/O 操作,比如读取用户输入、数据库读取和文件操作。 在等待期间,它可以释放锁,以便其他线程可以运行。 等待时间也可以是一些简单的操作,如休眠几秒钟。
总结:当线程等待操作时,它会释放锁,从而使处理器内核能够切换到另一个线程。 等待期结束后,之前的线程恢复执行。 这个过程,处理器内核在多个线程之间切换,实现了多线程。✅
如果您想在应用程序中实现进程级别的并行性,可以考虑使用多进程。
Python 线程模块:入门
Python 自带了一个 threading
模块,您可以将其导入到 Python 脚本中。
import threading
要在 Python 中创建线程对象,可以使用 Thread
构造函数:threading.Thread(...)
。 这是满足大多数线程需求的通用语法:
threading.Thread(target=..., args=...)
其中,
target
是一个关键字参数,它代表 Python 中可调用的对象(例如,函数)。args
是目标函数接受的参数元组。
您需要 Python 3.x 才能运行本教程中的代码示例。 您可以下载代码并跟着操作。
如何在 Python 中定义和运行线程
让我们定义一个运行目标函数的线程。
目标函数为 some_func
。
import threading
import time
def some_func():
print("正在运行 some_func...")
time.sleep(2)
print("some_func 运行完成。")
thread1 = threading.Thread(target=some_func)
thread1.start()
print(threading.active_count())
让我们解析一下上面的代码片段都做了什么:
- 导入了
threading
和time
模块。 - 函数
some_func
包含描述性的print()
语句,和一个两秒的休眠操作:time.sleep(n)
使函数休眠 n 秒。 - 接下来,我们定义了一个线程
thread_1
,其目标函数为some_func
。threading.Thread(target=...)
创建了一个线程对象。 - 注意:指定函数名而不是函数调用; 使用
some_func
而不是some_func()
。 - 创建线程对象不会启动线程; 你需要在线程对象上调用
start()
方法才能启动它。 - 要获取当前活动线程的数量,我们使用
active_count()
函数。
Python 脚本在主线程上运行,我们创建了另一个线程 (thread1
) 来运行函数 some_func
,因此当前活动线程的数量为两个,如输出所示:
# 输出
正在运行 some_func...
2
some_func 运行完成。
仔细观察输出,你会看到当 thread1
启动时,第一个打印语句会运行。 但在休眠操作期间,处理器切换到主线程并打印出活动线程的数量 – 而无需等待线程 1 完成执行。
等待线程完成执行
如果你希望 thread1
完成执行,你可以在启动线程后调用其 join()
方法。 这将等待 thread1
完成执行,而不会切换到主线程。
import threading
import time
def some_func():
print("正在运行 some_func...")
time.sleep(2)
print("some_func 运行完成。")
thread1 = threading.Thread(target=some_func)
thread1.start()
thread1.join()
print(threading.active_count())
现在,thread1
在我们打印活动线程数之前已经完成了执行。 因此,只有主线程在运行,这意味着当前活动线程的数量为 1。✅
# 输出
正在运行 some_func...
some_func 运行完成。
1
如何在 Python 中运行多个线程
接下来,让我们创建两个线程来运行两个不同的函数。
这里,count_down
是一个函数,它接受一个数字作为参数,并从该数字倒数到零。
def count_down(n):
for i in range(n,-1,-1):
print(i)
我们定义 count_up
,另一个 Python 函数,从零数到给定的数字。
def count_up(n):
for i in range(n+1):
print(i)
📑 使用语法 range(start, stop, step)
的 range()
函数时,默认情况下会排除终点 stop
。
– 要从特定数字倒数到零,可以使用负步进值 -1
并将停止值设置为 -1
,以便包含零。
– 类似地,要数到 n
,必须将 stop
值设置为 n + 1
。因为 start
和 step
的默认值分别为 0 和 1,可以使用 range(n + 1)
得到序列 0 到 n。
接下来,我们定义两个线程,thread1
和 thread2
,分别运行函数 count_down
和 count_up
。 我们为这两个函数添加了打印语句和休眠操作。
创建线程对象时,请注意目标函数的参数应该指定为一个元组 – 传递给 args
参数。 因为这两个函数 (count_down
和 count_up
) 都接受一个参数,你必须在值后显式地插入逗号。 这确保了参数仍然作为元组传递,否则,后续的元素会被推断为 None
。
import threading
import time
def count_down(n):
for i in range(n,-1,-1):
print("运行线程1....")
print(i)
time.sleep(1)
def count_up(n):
for i in range(n+1):
print("运行线程2...")
print(i)
time.sleep(1)
thread1 = threading.Thread(target=count_down,args=(10,))
thread2 = threading.Thread(target=count_up,args=(5,))
thread1.start()
thread2.start()
在输出中:
- 函数
count_up
在thread2
上运行,从 0 开始数到 5。 - 函数
count_down
在thread1
上运行,从 10 倒数到 0。
# 输出
运行线程1....
10
运行线程2...
0
运行线程1....
9
运行线程2...
1
运行线程1....
8
运行线程2...
2
运行线程1....
7
运行线程2...
3
运行线程1....
6
运行线程2...
4
运行线程1....
5
运行线程2...
5
运行线程1....
4
运行线程1....
3
运行线程1....
2
运行线程1....
1
运行线程1....
0
你可以看到 thread1
和 thread2
交替执行,因为它们都涉及到等待操作(休眠)。 一旦 count_up
函数计数到 5 完成,thread2
就不再处于活动状态。 所以我们得到的输出只对应于 thread1
。
总结
在本教程中,你学习了如何使用 Python 的内置 threading
模块来实现多线程。 以下是关键要点的总结:
Thread
构造函数可以用来创建线程对象。 使用threading.Thread(target=<callable>, args=( <tuple of args> ))
创建一个线程,该线程会使用args
中指定的参数来运行目标可调用对象。- Python 程序在主线程上运行,所以你创建的线程对象是附加的线程。 你可以调用
active_count()
函数返回当前活动线程的数量。 - 你可以使用线程对象上的
start()
方法来启动一个线程,然后使用join()
方法等待它完成执行。
你可以通过调整等待时间、尝试不同的 I/O 操作等来编写更多的示例。 确保在即将到来的 Python 项目中实现多线程。 编码愉快!🎉