淘先锋技术网

首页 1 2 3 4 5 6 7

转载自: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()实例化了一个锁对象,锁对象有两个方法:acquirerelease,分别是获得锁和释放锁。当一个线程获得所时,另外一个线程在acquire处阻塞,直到当前锁执行release被释放后才可以和其他线程共同争夺锁。acquirerelease之间的代码段执行时不会切换到其他线程,保证了操作的完整性。

用锁也存在问题,首先就是性能问题,在上面的例子中,不使用锁运行的执行时间是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_somethingdo_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_something2do_something2获得lock2后,同样执行time.sleep(3)使得GIL释放去执行do_something1do_something1此时需要获得lock2才能继续执行,然而lock2do_something2处,未释放无法获得。同理do_something2需要获得的lock1do_something1处,也无法获得。所以就出现了两个线程互相等待的情况。如果将其中某个线程获得的锁的顺序交换,程序就能正常执行。

可以看出,使用锁机制很容易造成死锁,在使用锁的时候一定要小心。