如果大家對Python中的多線程編程不是很了解,推薦大家閱讀之前的兩篇文章:
一、什么是加鎖
首先舉一個很生活化的例子,比如我們很多人在排隊上公共廁所,一旦前面的小明進(jìn)去了,那么后面的同學(xué)理論上就不能再進(jìn)去了。但是如果后面的同學(xué)不知道小明現(xiàn)在在廁所里面,硬是推門進(jìn)去了,這樣機(jī)會顯得很尷尬。
小明為了不讓這么尷尬的局面產(chǎn)生,進(jìn)入廁所之后,把廁所門鎖上。這樣后面的同學(xué)想要推門進(jìn)去的時候,就會發(fā)現(xiàn)門已經(jīng)上鎖了,便知道里面是有人的。這個時候,后面的同學(xué)就會老實在外面等待了。
這是生活中的加鎖,那么Python中的加鎖是什么樣子的呢?其實Python中的加鎖場景和剛剛舉的上廁所的例子如出一轍。
比如有一個全局變量A,如果有一個線程x正在使用全局變量A(還有可能會修改它),那么其他線程理論上是不能使用變量A的。但是其他線程并不知道x正在使用變量A,可能也會使用它,甚至還很有可能修改它。那么這個時候,就可能會出現(xiàn)問題。
為了不讓這些可能的問題出現(xiàn),線程x在使用變量A的時候,會給它加一把鎖,其他線程來使用A的時候,發(fā)現(xiàn)A已經(jīng)被鎖上了,就知道其他線程正在使用它,那么這個該線程就會老實地等待其他線程使用完畢,把鎖給打開之后,再來使用變量A。
二、為什么要加鎖
看了上面的文字,我們也大概知道了為什么要加鎖:為了避免可能出現(xiàn)的尷尬問題。這種尷尬問題在Python中的一個直接表現(xiàn)就是產(chǎn)生了“臟數(shù)據(jù)”。
什么是“臟數(shù)據(jù)”呢?這里不給大家列出官方的定義,給大家舉個實際的例子。
比如現(xiàn)在有兩個線程x和y,以及一個全局變量A,A的初始值是0。現(xiàn)在線程x和y做的工作是:循環(huán)執(zhí)行 A = A + 1 100次。示例代碼如下:
import threading
A = 0
def x():
global A
for i in range(100):
A = A + 1
print("x執(zhí)行完成之后,A=" + str(A))
def y():
global A
for i in range(100):
A = A + 1
print("y執(zhí)行完成之后,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分別循環(huán)執(zhí)行100次 A = A + 1 ,那么最終A是等于200。這個看上去很是理所當(dāng)然,沒什么問題。

但是如果如果線程x和y分別循環(huán)執(zhí)行100萬次 A = A + 1,最終A還是等于200萬嗎?這個就不一定了,或者說根本就不會等于200萬。

為什么會出現(xiàn)這個情況,就是因為線程x和線程y是同時執(zhí)行的,產(chǎn)生了臟數(shù)據(jù),才會導(dǎo)致A的值沒有達(dá)到我們理想中的那么多。
最后再補充一點:臟數(shù)據(jù)具體是怎么產(chǎn)生的呢?
線程x和線程y是同時在執(zhí)行的,很有可能會出現(xiàn)這個情況:
現(xiàn)在全局變量A=10,x正在執(zhí)行 A = A + 1,但是還沒有完成這一條命令,只完成了A+1,沒有將這個值賦給A,也就是說此時的A還是等于10。線程y也來了,他不知道線程x正在執(zhí)行 A = A + 1 ,于是就一把過A給拉過來,完整地執(zhí)行了 A = A + 1 ,此時A的值等于11。這個時候,線程x以為自己成功的執(zhí)行了一次 A = A + 1 ,便不再理會這一次的賦值,開始下一輪循環(huán)。
就這樣,線程x和線程y分別執(zhí)行了一次 A = A + 1 ,但是最終A的值只增加了1。在這個過程中那些完成了A+1,但是還沒來得及賦給A的數(shù)值,就是臟數(shù)據(jù)。
三、在Python中實現(xiàn)加鎖
嘮嘮叨叨這么多,那么在Python的具體代碼中,該如何實現(xiàn)加鎖呢?
在Python中實現(xiàn)加鎖是非常方便的,主要使用到 threading 庫中的Lock類。加鎖的思路只有三步:創(chuàng)建鎖,加鎖,釋放鎖。這三步思路放大我們的Python代碼中,就是下面的三行代碼:
gLock = threading.Lock(); # 創(chuàng)建一把鎖
gLock.acquire() # 上鎖
gLock.release() # 釋放鎖
就拿上面含有線程x和線程y的例子來說,線程中循環(huán)執(zhí)行100萬次 A = A + 1 ,我們使用加鎖機(jī)制,每次執(zhí)行這行代碼的之前都加上鎖,這樣另外一個線程就不能再來使用全局變量A,這行代碼執(zhí)行完之后就釋放鎖,這樣另外一個進(jìn)行就有可能來使用全局變量。具體的實現(xiàn)代碼如下:
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執(zhí)行完成之后,A=" + str(A))
def y():
global A
for i in range(1000000):
gLock.acquire() # 加鎖
A = A +1
gLock.release() # 釋放鎖
print("y執(zhí)行完成之后,A=" + str(A))
def main():
t1 = threading.Thread(target=x)
t2 = threading.Thread(target=y)
t1.start()
t2.start()
if __name__ == '__main__':
main()
如此一來,最后的運行結(jié)果如下圖。兩個線程執(zhí)行完畢之后,最終的A等于200萬。這就說明了加鎖的作用了。

最后補充一點:看到上圖,你可能會問:為什么線程x執(zhí)行完畢之后,A的數(shù)值不是100萬,二是非常接近200萬呢?
我們要知道,在上面的例子中,線程x和線程y是同時執(zhí)行的,而不是線程x執(zhí)行完成之后再來執(zhí)行線程y的。所以線程x執(zhí)行完成之前,其實是兩個線程同時執(zhí)行,同時對A執(zhí)行加一的操作,所以我們才會看到,線程x執(zhí)行完成之后,A的值已經(jīng)非常接近200萬。