如果大家對Python中的多線程程式設計不是很了解,推薦大家閱讀之前的兩篇文章:
- Python多線程程式設計——多線程基礎介紹
- Python多線程程式設計——建立線程的兩個方法
一、什麼是加鎖
首先舉一個很生活化的例子,比如我們很多人在排隊上公共廁所,一旦前面的小明進去了,那麼後面的同學理論上就不能再進去了。但是如果後面的同學不知道小明現在在廁所裡面,硬是推門進去了,這樣機會顯得很尴尬。
小明為了不讓這麼尴尬的局面産生,進入廁所之後,把廁所門鎖上。這樣後面的同學想要推門進去的時候,就會發現門已經上鎖了,便知道裡面是有人的。這個時候,後面的同學就會老實在外面等待了。
這是生活中的加鎖,那麼Python中的加鎖是什麼樣子的呢?其實Python中的加鎖場景和剛剛舉的上廁所的例子如出一轍。
比如有一個全局變量A,如果有一個線程x正在使用全局變量A(還有可能會修改它),那麼其他線程理論上是不能使用變量A的。但是其他線程并不知道x正在使用變量A,可能也會使用它,甚至還很有可能修改它。那麼這個時候,就可能會出現問題。
為了不讓這些可能的問題出現,線程x在使用變量A的時候,會給它加一把鎖,其他線程來使用A的時候,發現A已經被鎖上了,就知道其他線程正在使用它,那麼這個該線程就會老實地等待其他線程使用完畢,把鎖給打開之後,再來使用變量A。
二、為什麼要加鎖
看了上面的文字,我們也大概知道了為什麼要加鎖:為了避免可能出現的尴尬問題。這種尴尬問題在Python中的一個直接表現就是産生了“髒資料”。
什麼是“髒資料”呢?這裡不給大家列出官方的定義,給大家舉個實際的例子。
比如現在有兩個線程x和y,以及一個全局變量A,A的初始值是0。現線上程x和y做的工作是:循環執行
A = A + 1
100次。示例代碼如下:
import threading
A = 0
def x():
global A
for i in range(100):
A = A + 1
print("x執行完成之後,A=" + str(A))
def y():
global A
for i in range(100):
A = A + 1
print("y執行完成之後,A=" + str(A))
def main():
t1 = threading.Thread(target=x)
t2 = threading.Thread(target=y)
t1.start()
t2.start()
if __name__ == '__main__':
main()
如果線程x和y分别循環執行100次
A = A + 1
,那麼最終A是等于200。這個看上去很是理所當然,沒什麼問題。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIwQjMx8CX39CXy8CXycXZpZVZnFWbpN0NlAXayR3cvwFduVWay9WLvRXdh9CXyI3Zv1UZnFWbp9zZuBnL2ADNxUjNxETZ1UGMiVTZm1yMwYTO3gzMvw1cldWYtl2XkF2bsBXdvw1bp5SdoNnbhlmauMXZnFWbp1CZh9GbwV3Lc9CX6MHc0RHaiojIsJye.png)
但是如果如果線程x和y分别循環執行100萬次
A = A + 1
,最終A還是等于200萬嗎?這個就不一定了,或者說根本就不會等于200萬。
為什麼會出現這個情況,就是因為線程x和線程y是同時執行的,産生了髒資料,才會導緻A的值沒有達到我們理想中的那麼多。
最後再補充一點:髒資料具體是怎麼産生的呢?
線程x和線程y是同時在執行的,很有可能會出現這個情況:
現在全局變量A=10,x正在執行
A = A + 1
,但是還沒有完成這一條指令,隻完成了A+1,沒有将這個值賦給A,也就是說此時的A還是等于10。線程y也來了,他不知道線程x正在執行
A = A + 1
,于是就一把過A給拉過來,完整地執行了
A = A + 1
,此時A的值等于11。這個時候,線程x以為自己成功的執行了一次
A = A + 1
,便不再理會這一次的指派,開始下一輪循環。
就這樣,線程x和線程y分别執行了一次
A = A + 1
,但是最終A的值隻增加了1。在這個過程中那些完成了A+1,但是還沒來得及賦給A的數值,就是髒資料。
三、在Python中實作加鎖
唠唠叨叨這麼多,那麼在Python的具體代碼中,該如何實作加鎖呢?
在Python中實作加鎖是非常友善的,主要使用到 threading 庫中的Lock類。加鎖的思路隻有三步:建立鎖,加鎖,釋放鎖。這三步思路放大我們的Python代碼中,就是下面的三行代碼:
gLock = threading.Lock(); # 建立一把鎖
gLock.acquire() # 上鎖
gLock.release() # 釋放鎖
就拿上面含有線程x和線程y的例子來說,線程中循環執行100萬次
A = A + 1
,我們使用加鎖機制,每次執行這行代碼的之前都加上鎖,這樣另外一個線程就不能再來使用全局變量A,這行代碼執行完之後就釋放鎖,這樣另外一個進行就有可能來使用全局變量。具體的實作代碼如下:
import threading
A = 0
gLock = threading.Lock()
def x():
global A
for i in range(1000000):
gLock.acquire() # 加鎖
A = A +1
gLock.release() # 釋放鎖
print("x執行完成之後,A=" + str(A))
def y():
global A
for i in range(1000000):
gLock.acquire() # 加鎖
A = A +1
gLock.release() # 釋放鎖
print("y執行完成之後,A=" + str(A))
def main():
t1 = threading.Thread(target=x)
t2 = threading.Thread(target=y)
t1.start()
t2.start()
if __name__ == '__main__':
main()
如此一來,最後的運作結果如下圖。兩個線程執行完畢之後,最終的A等于200萬。這就說明了加鎖的作用了。
最後補充一點:看到上圖,你可能會問:為什麼線程x執行完畢之後,A的數值不是100萬,二是非常接近200萬呢?
我們要知道,在上面的例子中,線程x和線程y是同時執行的,而不是線程x執行完成之後再來執行線程y的。是以線程x執行完成之前,其實是兩個線程同時執行,同時對A執行加一的操作,是以我們才會看到,線程x執行完成之後,A的值已經非常接近200萬。