1、共享全局变量资源竞争
一个线程写入,一个线程读取,没问题,如果两个线程都写入呢?显然是会存在问题的,会使得写入的值达不到想要的结果;
Python中的dis模块中的dis方法可以查看一句Python代码的cpu运行轨迹,也就是cpu指令,
如果只是读取数据时,如读取一个函数,此时数据是安全的,因为没有涉及任何修改,
当改数据时,可能会涉及数据不安全,如当多个线程同时修改一个数据时,Python中的一句代码对应了多条cpu指令,假设有4条指令,当执行完第二条时,cpu时间片轮转了,此时数据可能发生错误。
所以任何 += -= *- 都是数据不安全的
只有load读数据,安全
import dis
def add_num():
num = 1
num += 1
dis.dis(add_num)
运行结果:
42 0 LOAD_CONST 1 (1)
2 STORE_FAST 0 (num)
43 4 LOAD_FAST 0 (num)
6 LOAD_CONST 1 (1)
8 INPLACE_ADD
10 STORE_FAST 0 (num)
12 LOAD_CONST 0 (None)
14 RETURN_VALUE
一个简单的赋值语句,对应的都是两个操作指令,一个是读取常量,一个是快速存储;如果再涉及到运算,有还会多一个运算的指令,而在多线程中,这些指令极有可能在某一条就中断了(cpu时间片轮转了),下次接着回来继续运行时,可能另一个线程中也完成了写的操作,这样就会导致两边写的数据最终结果是不一样的
import threading
num = 0
def demo1():
global num
for i in range(1000000):
num += 1
print(num)
def demo2():
global num
for i in range(1000000):
num += 1
print(num)
def main():
t1 = threading.Thread(target=demo1, name='domo1')
t2 = threading.Thread(target=demo2, name='domo2')
t1.start()
t2.start()
if __name__ == '__main__':
main()
运行结果:
1256870
1322058
如果是一个线程写完,另一个线程再写入,那么,结果应该是1000000,2000000,而现在的结果显然不是,原因就是线程在写入的过程中被中断了,去执行另一个线程,那么就有可能在存值的过程中被中断,这样导致的就是最终的结果少加了;
2、互斥锁(mutex)
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制;
某个线程要更改共享数据时,先将其锁定,此时资源的状态为”锁定”,其他线程不能改变,只到该线程释放资源,将资源的状态变成”非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
创建锁
mutex = threading.Lock()
加锁
mutex.acquire()
解锁
mutex.release()
import threading
num = 0
def demo1():
global num
mutex.acquire()
for i in range(2000000):
num += 1
mutex.release()
print(num)
def demo2():
global num
mutex.acquire()
for i in range(2000000):
num += 1
mutex.release()
print(2, num)
def main():
t1 = threading.Thread(target=demo1, name='domo1')
t2 = threading.Thread(target=demo2, name='domo2')
t1.start()
t2.start()
if __name__ == '__main__':
mutex = threading.Lock()
main()
注意:
- 加锁和解锁必须向对应,加多少个锁,解多少个锁,并且同一Lock()只能acquire一次,下一次acquire必须release后才能,不然会造成死锁;
- 使用RLock()方法,可以实现连着加锁,和连着解锁;
- acquire()可以设置加锁时间timeout;
- 在实现线程安全的控制中,比较常用的是 RLock;
3、死锁
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
import threading
import time
class MyThread1(threading.Thread):
def run(self):
# 对mutexA上锁
mutexA.acquire()
# mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁
print(self.name+'----do1---up----')
time.sleep(1)
# 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
mutexB.acquire()
print(self.name+'----do1---down----')
mutexB.release()
# 对mutexA解锁
mutexA.release()
class MyThread2(threading.Thread):
def run(self):
# 对mutexB上锁
mutexB.acquire()
# mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁
print(self.name+'----do2---up----')
time.sleep(1)
# 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了
mutexA.acquire()
print(self.name+'----do2---down----')
mutexA.release()
# 对mutexB解锁
mutexB.release()
mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
3.1 避免死锁
- 程序设计时要尽量避免
- 添加超时时间等
4、Condition
互斥锁Lock和RLock只能提供简单的加锁和释放锁等功能,它们的主要作用是在多线程访问共享数据时,保护共享数据,保证数据和关键代码的完整性。在此基础上,Python提供了Condition类,Condition类不仅自身依赖于Lock和RLock,即具有它们的阻塞特性,此外还提供了一些有利于线程通信,以及解决复杂线程同步问题的方法,它也被称作条件变量。
相关方法:
- acquire():加锁
- release():解锁
- wait(timeout):将线程挂起,直到收到一个notify通知或者等待时间超出timeout才会被唤醒,自动解锁;wait()必须在先Lock的前提下调用,否则会引起RuntimeError错误。
- notify(n=1):唤醒在Condition的waiting池中的n(参数n可设置,默认为1)个正在等待的线程并通知它,收通知的线程将自动调用acquire()方法尝试加锁;如果waiting池中有多个线程,随机选择n个唤醒;必须在已获得Lock的前提下调用,否则将引发错误。
- notify_all():唤醒waiting池中的等待的所有线程并通知它们。
- 可以用上下文管理器(with)的方法来使用cond,使用with,则会自动加解锁,无需acquire和release。
5、Queue线程
在线程中,访问一些全局变量,加锁是一个经常的过程。如果你是想把一些数据存储到某个队列中,那么Python内置了一个线程安全的模块叫做queue模块。Python中的queue模块中提供了同步的、线程安全的队列类,包括FIFO(先进先出)队列Queue,LIFO(后入先出)队列LifoQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么都做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。
初始化Queue(maxsize):创建一个先进先出的队列,maxsize:队列的长度,不定义理论上无限大(由自身计算机性能决定)。
qsize():返回队列的大小。
empty():判断队列是否为空,返回布尔值。
full():判断队列是否满了,返回布尔值。
get():从队列中取一个数据,队列空则阻塞,可设置阻塞时间timeout。
put():将一个数据放到队列中,队列满则阻塞,可设置阻塞时间timeout。
get_nowait():从队列中取一个数据,队列空直接抛出异常。
put_nowait():将一个数据放到队列中,队列满直接抛出异常。
线程同步
实现
天猫精灵:小爱同学
小爱同学:在
天猫精灵:现在几点了?
小爱同学:你猜猜现在几点了
注意小爱和天猫的启动顺序,先wait()的先启动;如果先启动notify的线程,会导致notify没有唤醒的对象,就进入了wait的挂起状态,然后就两个线程都是挂起,程序阻塞了;
import threading
from threading import Condition
class Xiaoai(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
cond.acquire()
cond.wait()
print("{}:在!".format(self.name))
cond.notify()
cond.wait()
print("{}:你猜猜现在几点了。".format(self.name))
cond.release()
class Tianmao(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
# cond.acquire()
with cond:
print("{}:小爱同学。".format(self.name))
cond.notify()
cond.wait()
print("{}:现在几点了?".format(self.name))
cond.notify()
# cond.release()
if __name__ == '__main__':
cond = Condition()
xiaoai = Xiaoai("小爱")
tianmao = Tianmao("天猫")
xiaoai.start()
tianmao.start()