前言
尽管Python中的线程有些鸡肋,但在IO操作中,提速显然。然而线程存在一个缺点,你可能不得不费点心力去关注线程同步的问题。这时我们需要用到线程锁。
为什么需要线程锁
这里有如下一段代码:
import threading
def increase():
global num
for i in range(1000000):
num += 1
def decrease():
global num
for i in range(1000000):
num -= 1
if __name__ == "__main__":
num = 0
lock = threading.Lock()
inc = threading.Thread(target=increase)
dec = threading.Thread(target=decrease)
inc.start()
dec.start()
inc.join()
dec.join()
print(num)
函数increase()总是负责对num加1,函数decrease()总是负责对num减1。然而,上述代码的运行结果总不为0,甚至多次运行的结果也总不一致。
为了探究num不为0的原因,我们可以深入到Python编译后的字节码中去:
def increase():
global num
num += 1
if __name__ == "__main__":
import dis
num = 0
print(dis.dis(increase))
# 输出(以下输出内容为Python编译后的字节码):
30 0 LOAD_GLOBAL 0 (num)
2 LOAD_CONST 1 (1)
4 INPLACE_ADD
6 STORE_GLOBAL 0 (num)
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
None
可以看到运行num += 1
时,Python解释器的执行步骤为:加载全局变量num(LOAD_GLOBAL),加载常量1(LOAD_CONST),相加并且赋值给num(INPLACE_ADD),存储全局变量(STORE_GLOBAL)…… 尽管Python因GIL锁缘故,总是只有一个线程在运行,但多个线程会交叉运行。因此它们会先后加载变量num,然后再去先后存储全局变量num。
细节是这样的:当num = 10时,increase(),decrease()先后加载了num,此时它们拿到的num都是100;经过increase()处理后num_local = 101,经过decrease()处理后num_local = 99(num_local表示执行STORE_GLOBA之前的num);假设increase()先执行存储操作,那么执行之后全局的num = 101,接着decrease()执行了存储操作,全局num = 99;最终num等于99。
为了避免此问题发生,我们需要线程锁。
Lock
实例一个线程锁:
lock = threading.Lock()
对象lock有两个重要方法,一个是acquire()
,一个是release()
,前者表示上锁,后者表示释放锁。有一点需要注意:同一个线程中,上锁之前锁必须是解开的,不然会造成死锁。开头那段代码修改后成了下面这样:
import threading
def increase(lock):
global num
for i in range(1000000):
lock.acquire()
num += 1
lock.release()
def decrease(lock):
global num
for i in range(1000000):
lock.acquire()
num -= 1
lock.release()
if __name__ == "__main__":
num = 0
lock = threading.Lock()
inc = threading.Thread(target=increase, args=(lock,))
dec = threading.Thread(target=decrease, args=(lock,))
inc.start()
dec.start()
inc.join()
dec.join()
print(num)
此时程序的运行结果总为0,但程序的运行速度也变慢了。这是因为解释器执行到acquire()
时会锁住共享资源。假设increase()先执行到了acquire()
(锁住资源),它就拥有先对num操作的权力,直到它执行release()
后(释放资源),其他线程才可以使用num,也就是说LOAD_GLOBAL->STORE_GLOBAL这个过程从原本的异步变成了同步,所以程序的运行速度变慢。
查看源码发现Lock还实现了__enter__
和__exit__
,因此我们可以用with进行上下文管理,代码得到简化:
...
def increase(lock):
global num
for i in range(1000000):
with lock:
num += 1
...
判断锁是否为“锁住状态”,可用locked()
:
import threading
lock = threading.Lock()
lock.acquire() # 上锁
print(lock.locked()) # 输出:True
lock.release() # 解锁
print(lock.locked()) # 输出:False
RLock
RLock是可重入锁,它与Lock最大的区别在于,RLock允许同一个线程中重复上锁,程序不会发生死锁:
import threading
lock = threading.RLock()
print("step1...")
lock.acquire()
lock.acquire()
print("step2...")
lock.release()
print("step3...")
lock.release()
# 输出:
step1...
step2...
step3...
但需要注意的是,每个acquire()都应该有一个与之对应的release()。
以下是RLock中acquire()
的实现源码。它通过跟踪宿主线程获取me
来判断上锁的线程是不是自己,如果是,引用加1(self._count += 1
),如果不是,上锁(self._block.acquire(blocking, timeout)
)。源码中的self._block
其实就是Lock的实例对象。
def acquire(self, blocking=True, timeout=-1):
me = get_ident()
if self._owner == me:
self._count += 1
return 1
rc = self._block.acquire(blocking, timeout)
if rc:
self._owner = me
self._count = 1
return rc
总之,在通常情况下使用RLock比Lock更安全。
感谢
- 参考慕课Bobby老师课程Python高级编程和异步IO并发编程
还不快抢沙发