python 多线程
https://zhuanlan.zhihu.com/p/490353142
1. 进程vs线程
进程(process)指的是正在运行的程序的实例,即an instance of a computer that is being executed。用拆字法理解就是:进行中的程序。程序是一个没有生命的实体,只有处理器执行它的时候才能成为一个活动的实体,称之为进程。[1]
线程(thread)包含于进程之中,是操作系统能够进行运算调度的最小单元。一条进程中可以并发多个线程,而同一条线程将共享该进程中的全部系统资源。[2]
每个进程都有自己独立的地址空间、内存和数据栈,因此进程之间通讯不方便,要是用进程间通讯(InterProcess Communication, IPC)。而同一个进程中的线程共享资源,因此线程间通讯非常方便,但要注意数据同步与互斥的问题。
虽然一条进程中可以并发多个线程,但是对于单核CPU而言,同一时间CPU只能运行一个线程。
2. thread vs threading
Python处理线程的模块有两个:thread和threading。Python 3已经停用了thread模块[3],并改名为_thread模块。Python 3在_thread模块的基础上开发了更高级的threading模块,因此以下的讲解都是基于threading模块。
3. 如何创建一个线程?
根据threading底层代码的说明,创建一个线程通常有两种方法:(1)在实例化一个线程对象时,将要执行的任务函数以参数的形式传入;(2)继承Thread类的同时重写它的run方法。
threading.Class类
现在我准备创建两个线程,一个线程每隔一秒打印一个“1”,另一个线程每隔2秒打印一个“2”,如何创建并执行呢?两种方法如下:
3.1 方法一
import time
import threading
def printNumber(n: int) -> None:
while True:
print(n)
time.sleep(n)
for i in range(1, 3):
t = threading.Thread(target=printNumber, args=(i, ))
t.start()
运行结果如下,控制台会不停地、交错地打印“1”和“2”:
运行结果
3.2 方法二
import time
import threading
class MyThread(threading.Thread):
def __init__(self, n):
self.n = n
# 注意:一定要调用父类的初始化函数,否则否发创建线程
super().__init__()
def run(self) -> None:
while True:
print(self.n)
time.sleep(self.n)
for i in range(1, 3):
t = MyThread(i)
t.start()
运行结果如下,控制台会不停地、交错地打印“1”和“2”:
运行结果
4. 主线程和子线程
我们先把上述的代码简单做一下修改,让它在打印的同时打印活跃的线程个数,代码如下
import time
import threading
class MyThread(threading.Thread):
def __init__(self, n):
self.n = n
super().__init__()
def run(self) -> None:
while True:
_count = threading.active_count()
print(self.n, f"当前活跃的线程个数:{_count}")
time.sleep(self.n)
for i in range(1, 3):
t = MyThread(i)
t.start()
该代码执行结果如下:
这里活跃的线程个数怎么理解?
好那么问题来了:当我创建了线程1并开始执行的时候,程序却告诉我有2个活跃的线程呢?同样地,我最终只创建了2个线程,为什么程序却告诉我有3个活跃的线程呢?
让我们回到进程和线程的定义,当我们开始执行这个程序的时候,这个程序成为一个“有生命的”进程,进程至少有一个线程,这个线程就是主线程。当程序执行到第一次t.start()的时候,程序创建了一个子线程,此时活跃的线程个数是2。进一步,当执行第二次t.start()的时候,程序又创建了一个子线程,因此最终活跃的线程个数是3。
注意每个进程只有一个主线程。
5. 守护线程(Daemon Thread)
守护线程(Daemon Thread)也叫后台进程,它的目的是为其他线程提供服务。如果其他线程被杀死了,那么守护线程也就没有了存在的必要。因此守护线程会随着非守护线程的消亡而消亡。Thread类中,子线程被创建时默认是非守护线程,我们可以通过setDaemon(True)将一个子线程设置为守护线程。
我们把上面这个例子中创建的两个子线程改写为守护线程,看看会发生什么:
import time
import threading
class MyThread(threading.Thread):
def __init__(self, n):
self.n = n
super().__init__()
def run(self) -> None:
while True:
_count = threading.active_count()
print(self.n, f"当前活跃的线程个数:{_count}")
time.sleep(self.n)
for i in range(1, 3):
t = MyThread(i)
t.setDaemon(True)
t.start()
print("结束!")
运行结果如下:
和前面不同,程序打印完“结束!”彻底结束了
和前面完全不同的是:程序打印完“结束!”后就彻底结束了,不再打印任何内容。这是为什么呢?
因为当程序执行完print("结束!")以后,主线程就可以结束了,这时候被设定为守护线程的两个子线程会被杀死,然后主线程结束。
现在,如果我把两个子线程的其中一个设置为守护线程,另一个设置为非守护线程,会怎样呢?代码如下:
import time
import threading
class MyThread(threading.Thread):
def __init__(self, n):
self.n = n
super().__init__()
def run(self) -> None:
while True:
_count = threading.active_count()
print(self.n, f"当前活跃的线程个数:{_count}")
time.sleep(self.n)
for i in range(1, 3):
t = MyThread(i)
if i == 1:
t.setDaemon(True) # 将其中一个线程设置为守护线程
t.start()
print("结束!")
你可能会想,守护线程会被杀死,非守护线程继续执行。但实际情况并非如此,结果如下:
两个子线程都在继续执行
这是因为非守护线程作为前台程序还在继续执行,守护线程就还有“守护”的意义,就会继续执行。
需要注意的是:将子线程设置为守护线程必须在调用start()方法之前,否则回引发RuntimeError异常。
6. join()方法
join()会使主线程进入等待状态(阻塞),直到调用join()方法的子线程运行结束。同时你也可以通过设置timeout参数来设定等待的时间,如:
import time
import threading
class MyThread(threading.Thread):
def __init__(self, n):
self.n = n
super().__init__()
def run(self) -> None:
while True:
_count = threading.active_count()
print(f"线程-{self.n}", f"当前活跃的线程个数:{_count}")
time.sleep(self.n)
for i in range(1, 3):
t = MyThread(i)
t.start()
t.join(3)
执行结果如下:
通过join()方法实现了主线程阻塞
7. 数据安全与线程锁
现在假设你创建了两个子线程操作同一个全局变量number,number被初始化为0,两个子线程通过for循环对这个number进行+1,每个子线程循环1000000次,两个子线程同时进行。如果一切正常的话,最终这个number会变成2000000,然而现实并非如此。代码如下
import time
import threading
number = 0
class MyThread(threading.Thread):
def __init__(self, n):
self.n = n
super().__init__()
def run(self) -> None:
global number
for i in range(1000000):
number += 1
for i in range(1, 3):
t = MyThread(i)
t.start()
# 给5秒钟让两个子线程执行完毕
time.sleep(5)
# 确保两个子线程执行完毕
print("活跃的线程个数:", threading.active_count())
# 输出最终数值
print("number: ", number)
执行结果如下:
结果并不是2000000
这种情况称为“脏数据”。产生脏数据的原因是,当一个线程在对数据进行修改时,修改到一半时另一个线程读取了未经修改的数据并进行修改。如何避免脏数据的产生呢?一个办法就是用join方法,即先让一个线程执行完毕再执行另一个线程。但这样的本质是把多线程变成了单线程,失去了多线程的意义。另一个办法就是用线程锁,threading模块中有如下几种线程锁[4]:
7.1 Lock互斥锁
import time
import threading
number = 0
lock = threading.Lock() # 实例化一个锁
class MyThread(threading.Thread):
def __init__(self, n):
self.n = n
super().__init__()
def run(self) -> None:
global number
for i in range(1000000):
lock.acquire() # 开锁,只允许当前线程访问共享的数据
number += 1
lock.release() # 释放锁,允许其他线程访问共享数据
for i in range(1, 3):
t = MyThread(i)
t.start()
# 给5秒钟让两个子线程执行完毕
time.sleep(5)
# 确保两个子线程执行完毕
print("活跃的线程个数:", threading.active_count())
# 输出最终数值
print("number: ", number)
执行结果如下:
输出正常
7.2 RLock
RLock和Lock的用法相同,区别在于:Lock只能开一次然后释放一次,不能开多次,而RLock可以开多次,再进行多次释放[5]。当然需要注意的是:RLock中虽然可以开多次,但是acquire和release的次数必须对应。
7.3 Semaphore
BoundedSemaphore类可以设置同一时间更改数据的线程个数
import time
import threading
semaphore = threading.BoundedSemaphore(3)
class MyThread(threading.Thread):
def __init__(self, n):
self.n = n
super().__init__()
def run(self) -> None:
semaphore.acquire()
for i in range(100):
_count = threading.active_count() - 1
print(f"线程-{self.n}", f"当前活跃的子线程个数:{_count}")
time.sleep(1)
semaphore.release()
for i in range(1, 10):
t = MyThread(i)
t.start()
执行结果如下:
Semaphore设置了同时执行的线程的个数
7.4 Event
Event类会在全局定义一个Flag,当Flag=False时,调用wait()方法会阻塞所有线程;而当Flag=True时,调用wait()方法不再阻塞。形象的比喻就是“红绿灯”:在红灯时阻塞所有线程,而在绿灯时又会一次性放行所有排队中的线程。Event类有四个方法:
- set():将Flag设置为True
- wait():等待
- clear():将Flag设置为False
- is_set():返回bool值,判断Flag是否为True
Event的一个好处是:可以实现线程间通信,通过一个线程去控制另一个线程。
import time
import threading
event = threading.Event()
event.set() # 设定Flag = True
class MyThread(threading.Thread):
def __init__(self, n):
self.n = n
super().__init__()
def run(self) -> None:
if self.n in [3, 4]:
event.clear() # 设定Flag = False
event.wait() # 线程3和4进入等待
for i in range(2):
_count = threading.active_count() - 1
print(f"线程-{self.n}", f"当前活跃的子线程个数:{_count}")
time.sleep(2)
if self.n == 2 and i == 1:
# 通过线程2来控制线程3和4
event.set()
for i in range(1, 5):
t = MyThread(i)
t.start()
执行结果如下:
通过一个线程去控制另一个线程
8. 一些小技巧
8.1 with上下门管理器
在使用Lock和RLock时,正确的开锁-释放锁非常重要。通过with上下文管理器,可以保证线程锁被正确释放,而且代码也更加简洁。如:
import time
import threading
number = 0
lock = threading.Lock() # 实例化一个锁
class MyThread(threading.Thread):
def __init__(self, n):
self.n = n
super().__init__()
def run(self) -> None:
global number
for i in range(1000000):
with lock: # with上下文管理器
number += 1
for i in range(1, 3):
t = MyThread(i)
t.start()
# 给5秒钟让两个子线程执行完毕
time.sleep(5)
# 确保两个子线程执行完毕
print("活跃的线程个数:", threading.active_count())
# 输出最终数值
print("number: ", number)
8.2 Timer计时器
通过threading.Timer类可以实现n秒后执行某操作。注意一个timer对象相当于一个新的子线程。
for i in range(1, 5):
t = MyThread(i)
if i == 4:
timer = Timer(0.1, t.start) # 5秒后再开始线程4
timer.start()
else:
t.start()
参考
- ^https://baike.baidu.com/item/%E8%BF%9B%E7%A8%8B/382503#:~:text=%E8%BF%9B%E7%A8%8B%EF%BC%88Process%EF%BC%89%E6%98%AF%E8%AE%A1%E7%AE%97%E6%9C%BA%E4%B8%AD,%E8%BF%9B%E7%A8%8B%E6%98%AF%E7%A8%8B%E5%BA%8F%E7%9A%84%E5%AE%9E%E4%BD%93%E3%80%82
- ^https://baike.baidu.com/item/%E7%BA%BF%E7%A8%8B/103101
- ^https://peps.python.org/pep-3108/#obsolete
- ^https://www.liujiangblog.com/course/python/79
- ^https://stackoverflow.com/questions/22885775/what-is-the-difference-between-lock-and-rlock