为什么需要同步
同样举之前的例子,两个线程分别对同一个全局变量进行加减,得不到预期结果,代码如下:
total = 0
def add():
global total
for i in range(1000000):
total += 1
def desc():
global total
for i in range(1000000):
total -= 1
import threading
thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)
原因就是因为 +=
和 -=
并不是原子操作。可以使用dis
模块查看字节码:
import dis
def add(total):
total += 1
def desc(total):
total -= 1
total = 0
print(dis.dis(add))
print(dis.dis(desc))
# 运行结果如下:
# 3 0 LOAD_FAST 0 (total)
# 3 LOAD_CONST 1 (1)
# 6 INPLACE_ADD
# 7 STORE_FAST 0 (total)
# 10 LOAD_CONST 0 (None)
# 13 RETURN_VALUE
# None
# 5 0 LOAD_FAST 0 (total)
# 3 LOAD_CONST 1 (1)
# 6 INPLACE_SUBTRACT
# 7 STORE_FAST 0 (total)
# 10 LOAD_CONST 0 (None)
# 13 RETURN_VALUE
# None
可以看到 add()
函数虽然其中只有一行代码,但是字节码主要分为四个步骤:
- load 变量total
- load 常量 1
- 执行加法操作
- 对total进行赋值
同理,desc()
函数的步骤相同,只是第三步改为执行减法。
假设一种极端情况,开始total = 0,首先线程1 load 变量total,得到值为0,切换到线程2,同样的到total为0,再次切换线程1 load常量1,执行加法,给total赋值得到1;然后线程2也 laod常量1,执行减法,给total赋值为-1。最终total为-1,而不是预期的0。
期望中,必须在+=
操作结束后,才能执行-=
,所以线程同步的需求就出来了。
互斥锁Lock
threading
模块中提供了threading.Lock
类(互斥锁),基本用法如下:
import threading
lock = threading.Lock()
lock.acquire() # 获取锁
# dosomething…… # 临界区的代码只能被同时只能被一个线程运行
lock.release() # 释放锁
将上面的代码修改,即可得到正确结果:
import threading
total = 0
lock = threading.Lock()
def add(lock):
global total
for i in range(1000000):
lock.acquire()
total += 1
lock.release()
def desc(lock):
global total
for i in range(1000000):
lock.acquire()
total -= 1
lock.release()
thread1 = threading.Thread(target=add, args=(lock,))
thread2 = threading.Thread(target=desc, args=(lock,))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)
# 执行结果 0
可以看到,添加互斥锁之后,程序执行结果是正确的,但是用了互斥锁之后,同样有一些缺陷:
- 添加锁之后,会影响程序性能。
- 可能引起"死锁"。
死锁主要在两种情况下发生:
-
迭代死锁
一个线程“迭代”请求同一个资源 ,会造成死锁。
lock = threading.Lock() lock.acquire() lock.acquire() total += 1 lock.release() lock.release()
上例种,第一次请求资源后还未 release ,再次acquire,最终无法释放,造成死锁 。(可通过可重入锁解决这个问题)。
-
互相调用死锁
两个线程中都会调用相同的资源,互相等待对方结束的情况 。假设A线程需要资源a,b,B线程也需要资源a,b,而线程A先获取资源a后,再获取资源b, B线程先获取资源b,再获取资源a。在A线程获取资源a,B线程获取资源b后,A线程在等待B线程释放资源b,而B线程在等待A线程释放资源a,从而死锁就发生了。
import threading import time lock_a = threading.Lock() lock_b = threading.Lock() def func1(): global lock_a global lock_b lock_a.acquire() time.sleep(1) lock_b.acquire() time.sleep(1) lock_b.release() lock_a.release() def func2(): global lock_a global lock_b lock_b.acquire() time.sleep(1) lock_a.acquire() time.sleep(1) lock_a.release() lock_b.release() thread1 = threading.Thread(target=func1) thread2 = threading.Thread(target=func2) thread1.start() thread2.start() thread1.join() thread2.join() print("program finished") # 程序会陷入死循环
这个例子比较重要,开始理解错了,如果B线程获取了资源b,然后释放之后再获取资源a,这样是不会发生死锁的。只有在B线程获取了资源b,还没有释放的时候,获取了资源a,才会发生死锁。
可重入锁RLock
为解决同一线程种不能多次请求同一资源的问题,python
提供了“可重入锁”:threading.RLock
,RLock
内部维护着一个Lock
和一个counter
变量,counter
记录了acquire
的次数,从而使得资源可以被多次require
。直到一个线程所有的acquire
都被release
,其他的线程才能获得资源 。用法和threading.Lock
类相同。
将上面迭代死锁的代码改写一下,就不会发生死锁,但注意,调用acquire
和release
的次数必须相等。
lock = threading.RLock()
lock.acquire()
lock.acquire()
total += 1
lock.release()
lock.release()
一般不会写这么无聊的代码,但是有一种情况是可能发生的,在加锁区域调用了某个函数,而这个函数内部又申请了同样的资源。
lock = threading.RLock()
def dosomething(lock):
lock.acquire()
# do something
lock.release()
lock.acquire()
dosomething(lock)
lock.release()
总结
- 线程间访问同一变量需要同步。
- 线程间加锁会导致性能损失。
- 加锁可能产生死锁,迭代死锁和互相调用死锁。
- 可重入锁可以避免迭代死锁。