线程同步
Lock、Rlock锁机制
使用锁的原因
为了避免线程间进行数据竞争,有时必须使用一些机制来强制线程同步。因为Cpython解释器中GIL(全局解释锁)的存在,在每一时刻只有一个线程在CPU中执行,每个线程执行了一定数量的字节码或者过了一定的时间切片再或者遇到了IO操作,CPU就会切换其他线程执行部分字节码。那么如果使用两个线程分别执行add()、reduce()方法,在理论情况下最后的结果num可能是0、1、-1,因为代码分解为字节码时,就分解成了很多步骤,有可能某个线程只执行了一些字节码,又去执行另一个线程的字节码,这样就可能造成结果的错乱。
import dis
num = 0
def add(num):
num += 1
return num
def reduce(num):
num -= 1
return num
print(dis.dis(add))
print(dis.dis(reduce))
下面就是add()、reduce()方法执行的字节码,在极端情况下,如果add、reduce每执行一个字节码就切换一次,那么结果就不0,就是1或者-1
5 0 LOAD_FAST 0 (num) 加载变量num到内存
2 LOAD_CONST 1 (1) 加载变量1到内存
4 INPLACE_ADD 执行加法
6 STORE_FAST 0 (num) 将结果赋值给num
6 8 LOAD_FAST 0 (num)
10 RETURN_VALUE None
10 0 LOAD_FAST 0 (num)
2 LOAD_CONST 1 (1)
4 INPLACE_SUBTRACT
6 STORE_FAST 0 (num)
11 8 LOAD_FAST 0 (num)
10 RETURN_VALUE None
使用锁的方式
为了避免上面出现的情况,必须给某些代码段加上锁
import threading
num = 0
def add(lock):
global num
for i in range(1000000):
lock.acquire()
num += 1
lock.release()
def reduce(lock):
global num
for i in range(1000000):
lock.acquire()
num -= 1
lock.release()
# 创建锁
lock = threading.Lock()
# 创建线程
t1 = threading.Thread(target=add, args=(lock, ))
t2 = threading.Thread(target=reduce, args=(lock, ))
t1.start()
t2.start()
# 等待子进程执行完
t1.join()
t2.join()
print(num)
这样结果就是0,但是加锁虽然达到了我们期望的结果,但是执行程序花费的时间却长了。
可重入的锁
在一个线程中,我们在部分代码中使用了锁lock.acquire()还没有释放锁时,调用了另一个方法,方法中又使用了锁,这样如果使用同一把普通的锁很容易出现死锁。这时我们可以使用Rlock,但是要注意使用了几次Rlock.acquire(),就必须使用几次Rlock.release(),下面是示例代码
import threading
num = 0
def add(rlock):
global num
for i in range(1000000):
rlock.acquire()
num += 1
dosomething(rlock)
rlock.release()
def reduce(rlock):
global num
for i in range(1000000):
rlock.acquire()
num -= 1
dosomething(rlock)
rlock.release()
def dosomething(rlock):
# 模拟其他操作的函数
rlock.acquire()
# dosomething
rlock.release()
# 创建锁
rlock = threading.RLock()
# 创建线程
t1 = threading.Thread(target=add, args=(rlock,))
t2 = threading.Thread(target=reduce, args=(rlock,))
t1.start()
t2.start()
# 等待子进程执行完
t1.join()
t2.join()
print(num)
使用锁的坏处
因为创建锁解锁都要花费时间,会影响程序性能;并且在使用锁时,容易产生死锁。
产生死锁的方式:1.创建锁,没有解锁 ; 2.两个线程互相有对方的锁,然后互相等待。
Condition机制
条件同步,用户复杂的线程间同步。内部使用的也是lock或者Rlock
重要的方法
-
wait(timeout=None)
当前线程等待着另外的线程发起通知,才向下执行。
-
notify()
发出通知
示例代码
需要了解的知识都在代码中的注释
import threading
"""
张三:床前明月光
李四:疑是地上霜
张三:举头望明月
李四:低头思故乡
怎么实现两个线程的交替说话,如果只有两句,可以使用锁机制,让某个线程先执行,这里有多句话交替出现,最好使用condition
"""
class ZSThead(threading.Thread):
def __init__(self, name, cond):
super(ZSThead, self).__init__()
self.name = name
self.cond = cond
def run(self):
# 必须先调用with self.cond,才能使用wait()、notify()方法
with self.cond:
# 讲话
print("{}:床前明月光".format(self.name))
# 等待李四的回应
self.cond.notify()
self.cond.wait()
# 讲话
print("{}:举头望明月".format(self.name))
# 等待李四的回应
self.cond.notify()
self.cond.wait()
class LSThread(threading.Thread):
def __init__(self, name, cond):
super(LSThread, self).__init__()
self.name = name
self.cond = cond
def run(self):
with self.cond:
# wait()方法不仅能获得一把锁,并且能够释放cond的大锁,这样张三才能进入with self.cond中
self.cond.wait()
print(f"{self.name}:疑是地上霜")
# notify()释放wait()生成的锁
self.cond.notify()
self.cond.wait()
print(f"{self.name}:低头思故乡")
self.cond.notify()
cond = threading.Condition()
zs = ZSThead("张三", cond)
ls = LSThread("李四", cond)
# 启动顺序很重要,必须先启动李四,让他在那里等待着,
# 因为先启动张三时,他说了话就发出了通知,但是当时李四的进程还没有启动,
# 并且condition外面的大锁也没有释放,李四也没法获取self.cond这把大锁
# condition有两层锁,一把底层锁在线程调用了wait()方法就会释放
# 每次调用wait()方法后,都会创建一把锁放进condition的双向队列中,等待notify()方法的唤醒
ls.start()
zs.start()