天天看點

Python多線程程式設計——多線程程式設計中的加鎖機制

如果大家對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。這個看上去很是理所當然,沒什麼問題。

Python多線程程式設計——多線程程式設計中的加鎖機制

但是如果如果線程x和y分别循環執行100萬次

A = A + 1

,最終A還是等于200萬嗎?這個就不一定了,或者說根本就不會等于200萬。

Python多線程程式設計——多線程程式設計中的加鎖機制

為什麼會出現這個情況,就是因為線程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萬。這就說明了加鎖的作用了。

Python多線程程式設計——多線程程式設計中的加鎖機制

最後補充一點:看到上圖,你可能會問:為什麼線程x執行完畢之後,A的數值不是100萬,二是非常接近200萬呢?

我們要知道,在上面的例子中,線程x和線程y是同時執行的,而不是線程x執行完成之後再來執行線程y的。是以線程x執行完成之前,其實是兩個線程同時執行,同時對A執行加一的操作,是以我們才會看到,線程x執行完成之後,A的值已經非常接近200萬。