技术交流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