技术交流QQ群:1027579432,欢迎你的加入!

1.锁的形象解释

有一个奇葩的房东,他家里有两个房间想要出租。这个房东很抠门,家里有两个房间,但却只有一把锁,不想另外花钱是去买另一把锁,也不让租客自己花钱加锁。这样租客只有先租到的那个人才能分配到锁。X先生,率先租到了房子,并且拿到了锁。而后来者Y先生,由于锁已经已经被X取走了,自己拿不到锁,也不能自己加锁,Y就不愿意了,也就不租了。换作其他人也一样,没有人会租第二个房间,直到X先生退租,把锁还给房东,可以让其他房客来取,第二间房间才能租出去。
换句话说,就是房东同时只能出租一个房间,一但有人租了一个房间,拿走了唯一的锁,就没有人再在租另一间房了。
回到线程中来,假设有两个线程A和B,A和B里的程序都加了同一个锁对象,当线程A率先执行到lock.acquire()(拿到全局唯一的锁后),线程B只能等到线程A释放锁lock.release()后(归还锁)才能运行lock.acquire()(拿到全局唯一的锁)并执行后面的代码。

2.如何使用"锁"?

import threading

# 生成锁对象,全局唯一
lock = threading.Lock()
# 获取锁,没有获得到锁的程序会陷入阻塞,直到程序重新获取到锁才能往下执行
lock.acquire()
# 释放锁,此时,其他程序可以使用锁了
lock.release()

注意:lock.acquire()与lock.release()必须成对使用,否则会造成死锁!!!为了有时候忘记,推荐使用上下文管理器来加锁,类似于tensorflow中的with tf.Session() as sess:

lock = threading.Lock()
with lock:
    # 写自己的业务逻辑代码
    pass

上面的with语句会在代码执行前自动获取锁,在执行结束后自动释放锁

3.可重入锁(RLock)

有时候在同一个线程中,我们可能会多次请求同一资源(就是,获取同一个锁的钥匙),俗称锁嵌套。如果还是按照常规的做***造成死锁的。比如,下面这段代码,你可以试着运行一下,会发现并没有输出结果。

import threading

def main():
    n = 0
    lock = threading.Lock()
    with lock:
        for i in range(10):
            n += 1
            with lock:
                print(n)

t1 = threading.Thread(target=main)
t1.start()

原因:在第二次获取锁时,发现锁已经被同一线程的人拿走了,自己也就理所当然拿不到锁了,所以程序卡住了。
解决方法:threading模块除了提供Lock锁之外,还提供了一种可重入锁RLock,专门来处理这个问题。

import threading

def main():
    n = 0
    lock = threading.RLock()    # 生成可重入锁对象
    with lock:
        for i in range(10):
            n += 1
            with lock:
                print(n)

t1 = threading.Thread(target=main)
t1.start()

注意: 可重入锁只能用在同一线程里,放松对锁钥匙的获取,其他与普通的Lock没啥不同。

4.防止死锁的加锁机制

死锁出现的情况:1.同一线程中,嵌套获取同一把锁,造成死锁;2.多个线程,不按顺序同时获取多个锁,造成死锁。例如:线程1:嵌套获取A,B两个锁;线程2:嵌套获取B,A两个锁。 由于两个线程是交替执行的,是有机会遇到线程1获取到锁A,而未获取到锁B,在同一时刻,线程2获取到锁B,而未获取到锁A。由于锁B已经被线程2获取了,所以线程1就卡在了获取锁B处,由于是嵌套锁,线程1未获取并释放B,是不能释放锁A的,这是导致线程2也获取不到锁A,也卡住了。两个线程,各执一锁,各不让步。造成死锁。
解决方法: 只要两个(或多个)线程获取嵌套锁时,按照固定顺序就能保证程序不会进入死锁状态。那么问题就转化成如何保证这些锁是按顺序的?(人工自觉,人工识别( 写一个辅助函数来对锁进行排序))

import threading
from contextlib import contextmanager

# 人工识别方法来排序
# Thread-local state to stored information on locks already acquired
_local = threading.local()

@contextmanager
def acquire(*locks):
    # Sort locks by object identifier
    locks = sorted(locks, key=lambda x: id(x))

    # Make sure lock order of previously acquired locks is not violated
    acquired = getattr(_local,'acquired',[])
    if acquired and max(id(lock) for lock in acquired) >= id(locks[0]):
        raise RuntimeError('Lock Order Violation')

    # Acquire all of the locks
    acquired.extend(locks)
    _local.acquired = acquired

    try:
        for lock in locks:
            lock.acquire()
        yield
    finally:
        # Release locks in reverse order of acquisition
        for lock in reversed(locks):
            lock.release()
        del acquired[-len(locks):]
# 使用上面定义的人工识别方法
import threading
x_lock = threading.Lock()
y_lock = threading.Lock()

def thread_1():

    while True:
        with acquire(x_lock):
            with acquire(y_lock):
                print('Thread-1')

def thread_2():
    while True:
        with acquire(y_lock):
            with acquire(x_lock):
                print('Thread-2')

t1 = threading.Thread(target=thread_1)
t1.daemon = True
t1.start()

t2 = threading.Thread(target=thread_2)
t2.daemon = True
t2.start()

分析:表面上thread_1的先获取锁x,再获取锁y,而thread_2是先获取锁y,再获取x。 但是实际上,acquire函数,已经对x,y两个锁进行了排序。所以thread_1,hread_2都是以同一顺序来获取锁的,是不会造成死锁的。

5.GIL(全局锁)

多进程是真正的并行,而多线程是伪并行,实际上它只是交替执行。由于GIL导致多线程实际上是伪并行的。因为任何Python线程执行前,必须先获得GIL锁。然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁。所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
注意:GIL并不是Python的特性,它是在实现Python解释器(CPython)时所引入的一个概念。而Python解释器,并不是只有CPython。除它之外,还有PyPy,Psyco,JPython,IronPython等。在绝大多数情况下,我们通常都认为 Python == CPython,所以也就默许了Python具有GIL锁这个事。
解决方法: 1.使用多进程代替多线程;2.更换Python解释器,不使用CPython

6.参考博客链接

python编程时光
进程与线程的一个简单解释