你們好,併發編程
進入第三篇。編程
今天咱們來說講,線程裏的鎖機制
。多線程
何爲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
好了,關於線程的鎖機制,咱們大概就介紹這些內容。