你们好,并发编程
进入第三篇。编程
今天咱们来说讲,线程里的锁机制
。多线程
何为Lock( 锁 )?并发
如何使用Lock( 锁 )?ide
为什么要使用锁?函数
可重入锁(RLock)性能
防止死锁的加锁机制学习
饱受争议的GIL(全局锁)ui
何为 Lock
( 锁 ),在网上找了好久,也没有找到合适的定义。可能锁
这个词已经足够直白了,不须要再解释了。spa
可是,对于新手来讲,我仍是要说下个人理解。线程
我本身想了个生活中例子来看下。
有一个奇葩的房东,他家里有两个房间想要出租。这个房东很抠门,家里有两个房间,但却只有一把锁,不想另外花钱是去买另外一把锁,也不让租客本身加锁。这样租客只有,先租到的那我的才能分配到锁。X先生,率先租到了房子,而且拿到了锁。然后来者Y先生,因为锁已经已经被X取走了,本身拿不到锁,也不能本身加锁,Y就不肯意了。也就不租了,换做其余人也同样,没有人会租第二个房间,直到X先生退租,把锁还给房东,可让其余房客来取。第二间房间才能租出去。
换句话说,就是房东同时只能出租一个房间,一但有人租了一个房间,拿走了惟一的锁,就没有人再在租另外一间房了。
回到咱们的线程中来,有两个线程A和B,A和B里的程序都加了同一个锁对象,当线程A率先执行到lock.acquire()
(拿到全局惟一的锁后),线程B只能等到线程A释放锁lock.release()
后(归还锁)才能运行lock.acquire()(拿到全局惟一的锁)并执行后面的代码。
这个例子,是否是让你清楚了什么是锁呢?
来简单看下代码,学习如何获取锁,释放锁。
1import threading
2
3# 生成锁对象,全局惟一
4lock = threading.Lock()
5
6# 获取锁。未获取到会阻塞程序,直到获取到锁才会往下执行
7lock.acquire()
8
9# 释放锁,归回倘,其余人能够拿去用了
10lock.release()
须要注意的是,lock.acquire()
和 lock.release()
必须成对出现。不然就有可能形成死锁。
不少时候,咱们虽然知道,他们必须成对出现,可是仍是不免会有忘记的时候。
为了,规避这个问题。我推荐使用使用上下文管理器
来加锁。
1import threading
2
3lock = threading.Lock()
4with lock:
5 # 这里写本身的代码
6 pass
with
语句会在这个代码块执行前自动获取锁,在执行结束后自动释放锁。
你如今确定仍是一脸懵逼,这么麻烦,我不用锁不行吗?有的时候还真不行。
那么为了说明锁存在的意义。咱们分别来看下,不用锁的情形有怎样的问题。
定义两个函数,分别在两个线程中执行。这两个函数 共用
一个变量 n
。
1def job1():
2 global n
3 for i in range(10):
4 n+=1
5 print('job1',n)
6
7def job2():
8 global n
9 for i in range(10):
10 n+=10
11 print('job2',n)
12
13n=0
14t1=threading.Thread(target=job1)
15t2=threading.Thread(target=job2)
16t1.start()
17t2.start()
看代码貌似没什么问题,执行下看看输出
1job1 1
2job1 2
3job1 job2 13
4job2 23
5job2 333
6job1 34
7job1 35
8job2
9job1 45 46
10job2 56
11job1 57
12job2
13job1 67
14job2 68 78
15job1 79
16job2
17job1 89
18job2 90 100
19job2 110
卧槽,是否是很乱?彻底不是咱们预想的那样。
解释下这是为何?由于两个线程共用一个全局变量,又因为两线程是交替执行的,当job1
执行三次 +1
操做时,job2
就无论三七二十一 给n作了+10
操做。两个线程之间,执行彻底没有规矩,没有约束。因此会看到输出固然也很乱。
加了锁后,这个问题也就解决了,来看看
1def job1():
2 global n, lock
3 # 获取锁
4 lock.acquire()
5 for i in range(10):
6 n += 1
7 print('job1', n)
8 lock.release()
9
10
11def job2():
12 global n, lock
13 # 获取锁
14 lock.acquire()
15 for i in range(10):
16 n += 10
17 print('job2', n)
18 lock.release()
19
20n = 0
21# 生成锁对象
22lock = threading.Lock()
23
24t1 = threading.Thread(target=job1)
25t2 = threading.Thread(target=job2)
26t1.start()
27t2.start()
因为job1
的线程,率先拿到了锁,因此在for循环中,没有人有权限对n进行操做。当job1
执行完毕释放锁后,job2
这才拿到了锁,开始本身的for循环。
看看执行结果,真如咱们预想的那样。
1job1 1
2job1 2
3job1 3
4job1 4
5job1 5
6job1 6
7job1 7
8job1 8
9job1 9
10job1 10
11job2 20
12job2 30
13job2 40
14job2 50
15job2 60
16job2 70
17job2 80
18job2 90
19job2 100
20job2 110
这里,你应该也知道了,加锁是为了对锁内资源(变量)进行锁定,避免其余线程篡改已被锁定的资源,以达到咱们预期的效果。
为了不你们忘记释放锁,后面的例子,我将都使用with
上下文管理器来加锁。你们注意一下。
有时候在同一个线程中,咱们可能会屡次请求同一资源(就是,获取同一锁钥匙),俗称锁嵌套。
若是仍是按照常规的作法,会形成死锁的。好比,下面这段代码,你能够试着运行一下。会发现并无输出结果。
1import threading
2
3def main():
4 n = 0
5 lock = threading.Lock()
6 with lock:
7 for i in range(10):
8 n += 1
9 with lock:
10 print(n)
11
12t1 = threading.Thread(target=main)
13t1.start()
是由于,第二次获取锁时,发现锁已经被同一线程的人拿走了。本身也就理所固然,拿不到锁,程序就卡住了。
那么如何解决这个问题呢。
threading
模块除了提供Lock
锁以外,还提供了一种可重入锁RLock
,专门来处理这个问题。
1import threading
2
3def main():
4 n = 0
5 # 生成可重入锁对象
6 lock = threading.RLock()
7 with lock:
8 for i in range(10):
9 n += 1
10 with lock:
11 print(n)
12
13t1 = threading.Thread(target=main)
14t1.start()
执行一下,发现已经有输出了。
11
22
33
44
55
66
77
88
99
1010
须要注意的是,可重入锁,只在同一线程里,放松对锁的获取机制,容许同一线程里的屡次对锁进行获取,其余与Lock
并没有二致。
在编写多线程程序时,可能无心中就会写了一个死锁。能够说,死锁的形式有多种多样,可是本质都是相同的,都是对资源不合理竞争的结果。
以本人的经验总结,死锁一般如下几种
同一线程,嵌套获取同把锁,形成死锁。
多个线程,不按顺序同时获取多个锁。形成死锁
对于第一种,上面已经说过了,使用可重入锁。
主要是第二种。可能你还没明白,是如何死锁的。
举个例子。
线程1,嵌套获取A,B两个锁,线程2,嵌套获取B,A两个锁。
因为两个线程是交替执行的,是有机会遇到线程1获取到锁A,而未获取到锁B,在同一时刻,线程2获取到锁B,而未获取到锁A。因为锁B已经被线程2获取了,因此线程1就卡在了获取锁B处,因为是嵌套锁,线程1未获取并释放B,是不能释放锁A的,这是致使线程2也获取不到锁A,也卡住了。两个线程,各执一锁,各不让步。形成死锁。
通过数学证实,只要两个(或多个)线程获取嵌套锁时,按照固定顺序就能保证程序不会进入死锁状态。
那么问题就转化成如何保证这些锁是按顺序的?
有两个办法
人工自觉,人工识别。
写一个辅助函数来对锁进行排序。
第一种,就不说了。
第二种,能够参考以下代码
1import threading
2from contextlib import contextmanager
3
4# Thread-local state to stored information on locks already acquired
5_local = threading.local()
6
7@contextmanager
8def acquire(*locks):
9 # Sort locks by object identifier
10 locks = sorted(locks, key=lambda x: id(x))
11
12 # Make sure lock order of previously acquired locks is not violated
13 acquired = getattr(_local,'acquired',[])
14 if acquired and max(id(lock) for lock in acquired) >= id(locks[0]):
15 raise RuntimeError('Lock Order Violation')
16
17 # Acquire all of the locks
18 acquired.extend(locks)
19 _local.acquired = acquired
20
21 try:
22 for lock in locks:
23 lock.acquire()
24 yield
25 finally:
26 # Release locks in reverse order of acquisition
27 for lock in reversed(locks):
28 lock.release()
29 del acquired[-len(locks):]
如何使用呢?
1import threading
2x_lock = threading.Lock()
3y_lock = threading.Lock()
4
5def thread_1():
6
7 while True:
8 with acquire(x_lock):
9 with acquire(y_lock):
10 print('Thread-1')
11
12def thread_2():
13 while True:
14 with acquire(y_lock):
15 with acquire(x_lock):
16 print('Thread-2')
17
18t1 = threading.Thread(target=thread_1)
19t1.daemon = True
20t1.start()
21
22t2 = threading.Thread(target=thread_2)
23t2.daemon = True
24t2.start()
看到没有,表面上thread_1
的先获取锁x,再获取锁y
,而thread_2
是先获取锁y
,再获取x
。
可是实际上,acquire
函数,已经对x
,y
两个锁进行了排序。因此thread_1
,hread_2
都是以同一顺序来获取锁的,是否是形成死锁的。
在第一章的时候,我就和你们介绍到,多线程和多进程是不同的。
多进程是真正的并行,而多线程是伪并行,实际上他只是交替执行。
是什么致使多线程,只能交替执行呢?是一个叫GIL
(Global Interpreter Lock
,全局解释器锁)的东西。
什么是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锁这个事。
都知道GIL影响性能,那么如何避免受到GIL的影响?
使用多进程代替多线程。
更换Python解释器,不使用CPython
好了,关于线程的锁机制,咱们大概就介绍这些内容。