转载自:https://www.jianshu.com/p/9fa218e50a16
在Python多线程(一):GIL中我们提到了竞态条件问题,即不同线程修改相同的共享变量出现运行多次结果不一样的问题,即使CPython中有GIL,这种问题依然存在。现在我们通过多线程的锁机制来解决这个问题。
还是相同的代码:
import threading
total = 0
def add():
global total
for i in range(1000000):
total += 1
def desc():
global total
for i in range(1000000):
total -= 1
thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)
之前我们的分析的原因在于:两加法减法操作在底层实现的时候有多个步骤,由于GIL的切换导致字节码交替运行。如果我们能够保证实现加法或者减法操作的时候只有一个线程在运行,就能解决这个问题。而保证某一代码段只有一个线程在运行的方法就是为这个线程加锁,如下所示:
import threading
total = 0
lock = threading.Lock()
def add():
global total
global lock
for i in range(1000000):
lock.acquire()
total += 1
lock.release()
def desc():
global total
global lock
for i in range(1000000):
lock.acquire()
total -= 1
lock.release()
thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)
运行结果为0
。在上面的代码中,threading.Lock()
实例化了一个锁对象,锁对象有两个方法:acquire
和release
,分别是获得锁和释放锁。当一个线程获得所时,另外一个线程在acquire
处阻塞,直到当前锁执行release
被释放后才可以和其他线程共同争夺锁。acquire
和release
之间的代码段执行时不会切换到其他线程,保证了操作的完整性。
用锁也存在问题,首先就是性能问题,在上面的例子中,不使用锁运行的执行时间是0.15
秒,而使用锁执行时间是2.35
秒,足足慢了15
倍。
另外一个问题被称为死锁。当一个线程调用子程序时,如果这个子程序也需要加锁,则会出现这个问题:
import threading
import time
lock = threading.Lock()
def do_something():
global lock
lock.acquire()
do_sub_task()
lock.release()
def do_sub_task():
global lock
lock.acquire()
time.sleep(2)
lock.release()
thread = threading.Thread(target=do_something)
thread.start()
thread.join()
程序会在do_sub_task
的首句阻塞,因为该函数试图去获取锁,但是锁并没有释放。解决方法有两种:
一种是通过threading.Lock()
再实例化一把锁,使得do_something
和do_sub_task
所需要的锁不是同一把,这样即使do_something
获取了锁,do_sub_task
也能够获得另外的锁。但是这种方式的问题是当这种情况出现很多,锁就很难管理。
另外一种是使用threading.RLock
,这种锁可以重复获得,只要释放的次数等于获得的次数即可。将上面代码中的Lock
换成RLock
即可。
还有一种死锁情况称为互相等待,参看下面代码:
import threading
import time
lock1 = threading.Lock()
lock2 = threading.Lock()
def do_something1():
lock1.acquire()
time.sleep(2)
lock2.acquire()
print('Something 1 started')
time.sleep(2)
lock1.release()
lock2.release()
print('Something 1 ended')
def do_something2():
lock2.acquire()
time.sleep(3)
lock1.acquire()
print('Something 2 started')
time.sleep(3)
lock2.release()
lock1.release()
print('Something 2 ended')
thread1 = threading.Thread(target=do_something1)
thread2 = threading.Thread(target=do_something2)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
这个程序会一致阻塞,原因在于两个线程获得两个锁的顺序是相反的,当do_something1
运行时获得lock1
,然后执行time.sleep(2)
使得GIL
释放去执行do_something2
。do_something2
获得lock2后,同样执行time.sleep(3)
使得GIL
释放去执行do_something1
,do_something1
此时需要获得lock2
才能继续执行,然而lock2
在do_something2
处,未释放无法获得。同理do_something2
需要获得的lock1
在do_something1
处,也无法获得。所以就出现了两个线程互相等待的情况。如果将其中某个线程获得的锁的顺序交换,程序就能正常执行。
可以看出,使用锁机制很容易造成死锁,在使用锁的时候一定要小心。