Python线程:简介
在本教程中,您将学习如何使用Python内置的threading模块来探索Python中的多线程能力。
从进程和线程的基础知识开始,您将了解Python中的多线程工作原理,同时理解并发性和并行性的概念。然后,您将学习如何使用内置的threading
模块在Python中启动和运行一个或多个线程。
让我们开始吧。
进程 vs. 线程:区别
什么是进程?
进程是需要运行的程序的任何实例。
它可以是任何东西-比如一个Python脚本或一个网页浏览器(如Chrome)或视频会议应用程序。如果在计算机上启动任务管理器并导航到性能-> CPU,您将能够看到当前正在运行在CPU核心上的进程和线程。
了解进程和线程
在内部,进程有一个专用的内存,用于存储与进程对应的代码和数据。
一个进程由一个或多个线程组成。线程是操作系统可以执行的最小指令序列,它代表了执行的流程。
每个线程都有自己的堆栈和寄存器,但没有专用的内存。与进程相关联的所有线程都可以访问数据。因此,数据和内存由进程的所有线程共享。
在具有N个核心的CPU上,N个进程可以同时并行执行。但是,同一进程的两个线程永远不能同时执行,但可以并发执行。我们将在下一节讨论并发性与并行性的概念。
基于我们目前学到的知识,让我们总结一下进程和线程之间的区别。
特征 | 进程 | 线程 |
内存 | 专用内存 | 共享内存 |
执行模式 | 并行,同时发生 | 并发;但不是并行的 |
执行受 | 操作系统处理 | CPython解释器处理 |
Python中的多线程
在Python中,Global Interpreter Lock (GIL)确保只有一个线程可以获取锁并在任何时刻运行。所有线程都应该获取此锁才能运行。这确保了在任何给定的时间点上只能有一个线程在执行,并避免了同时多线程。
例如,考虑同一进程的两个线程t1
和t2
。因为线程共享相同的数据,当t1
正在读取特定值k
时,t2
可能会修改同样的值k
。这可能导致死锁和不希望的结果。但是在任何时刻只有一个线程可以获取锁并运行。因此,GIL也确保了线程安全。
那么我们如何在Python中实现多线程能力呢?为了理解这一点,让我们讨论并发性和并行性的概念。
并发性 vs. 并行性:概述
考虑一个具有多个核心的CPU。在下面的示例中,CPU有四个核心。这意味着我们可以在任何给定的时刻并行运行四个不同的操作。
如果有四个进程,那么每个进程都可以独立地并同时在四个核心上运行。假设每个进程都有两个线程。
为了理解线程如何工作,让我们从多核处理器架构切换到单核处理器架构。正如前面提到的,每次执行实例只能有一个线程处于活动状态,但处理器核心可以在不同线程之间切换。
例如,I/O绑定的线程通常会等待I/O操作:读取用户输入、数据库读取和文件操作。在这个等待时间内,I/O绑定的线程可以释放锁,以便其他线程可以运行。等待时间也可以是一个简单的操作,比如睡眠n秒。
总结一下:在等待操作期间,线程释放锁,使处理器核心可以切换到另一个线程。等待时间结束后,之前的线程恢复执行。处理器核心在不同线程之间并发切换的这个过程促进了多线程。
如果您想在应用程序中实现进程级并行性,请考虑使用multiprocessing。
Python线程模块:入门
Python附带了一个可以导入到Python脚本中的线程模块。
要在Python中创建一个线程对象,可以使用Thread构造函数:threading.Thread(…)。这是大多数线程实现所适合的通用语法:
threading.Thread(target=…,args=…)
在这里,
target是指定Python可调用对象的关键字参数
args是目标函数接收的参数元组。
要运行本教程中的代码示例,您需要Python 3.x和Download the code。
定义和运行Python线程
让我们定义一个运行目标函数的线程。
目标函数是some_func。
import threading
import time
def some_func():
print(“Running some_func…”)
time.sleep(2)
print(“Finished running some_func.”)
thread1 = threading.Thread(target=some_func)
thread1.start()
print(threading.active_count())
让我们解析一下上面的代码片段的功能:
导入threading和time模块。
函数some_func具有描述性的print()语句,并包括一个两秒的睡眠操作:time.sleep(n)使函数睡眠n秒。
接下来,我们使用some_func作为目标定义一个线程thread_1。threading.Thread(target=…)创建一个线程对象。
注意:指定函数的名称而不是函数调用;使用some_func而不是some_func()。
创建线程对象不会启动线程;调用线程对象上的start()方法会启动线程。
使用active_count()函数获取活动线程的数量。
Python脚本正在主线程上运行,我们正在创建另一个线程(thread1)来运行函数some_func;因此,活动线程数为2,如输出所示:
# 输出
Running some_func…
2
Finished running some_func.
如果我们仔细观察输出,可以看到在启动thread1后,第一个打印语句被执行。但是在睡眠操作期间,处理器切换到主线程并打印出活动线程的数量,而不等待thread1执行完成。
等待线程完成执行
如果您希望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()
函数的语法range(start, stop, step)
时,默认情况下,终点stop
是排除在外的。– 要从特定数字倒数到零,可以使用负数的
step
值为-1,并将stop
值设置为-1,以包括零。– 类似地,要计数到
n
,您必须将stop
值设置为n + 1
。因为start
和step
的默认值分别为0和1,所以您可以使用range(n + 1)
得到0到n的序列。
接下来,我们定义了两个线程thread1
和thread2
,分别运行函数count_down
和count_up
。我们为两个函数都添加了print
语句和sleep
操作。
在创建线程对象时,注意目标函数的参数应指定为元组,传递给args
参数。由于两个函数(count_down
和count_up
)都接受一个参数,您需要在值后显式插入逗号。这确保参数仍然作为元组传递,因为随后的元素被推断为None
。
import threading
import time
def count_down(n):
for i in range(n,-1,-1):
print("运行thread1....")
print(i)
time.sleep(1)
def count_up(n):
for i in range(n+1):
print("运行thread2...")
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。
# 输出
运行thread1....
10
运行thread2...
0
运行thread1....
9
运行thread2...
1
运行thread1....
8
运行thread2...
2
运行thread1....
7
运行thread2...
3
运行thread1....
6
运行thread2...
4
运行thread1....
5
运行thread2...
5
运行thread1....
4
运行thread1....
3
运行thread1....
2
运行thread1....
1
运行thread1....
0
你可以看到thread1
和thread2
交替执行,因为它们都涉及到等待操作(睡眠)。一旦count_up
函数计数到5,thread2
就不再活动。所以我们得到与thread1
相对应的输出。
总结
在本教程中,你学会了如何使用Python的内置线程模块实现多线程。以下是主要要点的总结:
- Thread构造函数可用于创建线程对象。使用threading.Thread(target=,args=())创建一个以target可调用对象和args参数指定的线程。
- Python程序在主线程上运行,因此你创建的线程对象是额外的线程。你可以调用active_count()函数在任何时刻返回活动线程的数量。
- 你可以使用线程对象上的start()方法启动线程,并使用join()方法等待它执行完成。
你可以通过调整等待时间、尝试不同的I/O操作等来进行调试。记得在即将到来的link_4
中实现多线程。愉快编码!🎉