天天看點

05-7 萬字長文:實作多線程(下)

05-7 萬字長文:實作多線程(下)

你好,我是悅創。線上程中有一個叫作守護線程的概念,如果一個線程被設定為守護線程,那麼意味着這個線程是“不重要”的,這意味着,如果主線程結束了而該守護線程還沒有運作完,那麼它将會被強制結束。在 python 中我們可以通過 setdaemon 方法來将某個線程設定為守護線程。

如果要修改成守護線程,那你就得在 thread.start() 前面加一個:

需要在我們啟動之前設定。

「示例一如下:」

添加之前:

添加之後:

我們可以看見,程式直接運作:start、stop,執行到 **print('stop') 它就結束了。**也就随着我們的主線程結束而結束。并不管它裡面還有什麼沒有執行完。(也不會管他裡面的 time.sleep())我們的主線程一結束,我們的守護線程就會随着主線程一起銷毀。

「我們日常啟動的是非守護線程,守護線程用的較少。」

守護線程會伴随主線程一起結束,setdaemon 設定為 true 即可。

「示例二如下:」

在這裡我們通過 setdaemon 方法将 t2 設定為了守護線程,這樣主線程在運作完畢時,t2 線程會随着線程的結束而結束。

運作結果:

可以看到,我們沒有看到 thread-2 列印退出的消息,thread-2 随着主線程的退出而退出了。

不過細心的你可能會發現,這裡并沒有調用 join 方法,如果我們讓 t1 和 t2 都調用 join 方法,主線程就會仍然等待各個子線程執行完畢再退出,不論其是否是守護線程。

接下來是比較難的知識點,還是從簡單的知識點開始。

「比方」說我們現在有兩個線程,一個是求加一千萬次,另一個是減一千萬次。按原本得計劃來說,一個加一千萬一個減一千萬結果應該還是零。可是最終得結果并不是等于零,我們多運作幾次會發現幾次得出來得結果并不相同。多線程代碼如下:

就算單線程也會出現兩個值:1000000 與 -1000000,兩個函數誰先運作就是輸出誰的結果,為什麼呢?因為兩個函數調用的是全局變量 「number」 是以,如果先運作加法函數,加法得到的結果是 1000000 ,那全局下的 number 的值也會變成:1000000 ,那減法的操作亦然就是 0。反過來也是一個意思。代碼如下:

由上面的多線程代碼,我可以發現結果:兩個線程操作同一個數字,最後得到的數字是混亂的。為什麼說是混亂的呢?

我們現在所要做的是一個指派,number += 1 其實也就是 number = number + 1,的這個操作。而在我們的 python 當中,我們是先:計算右邊的,然後指派給左邊的,一共兩步。

我先來看一下正确的運作流程:

上面的過成是正确的流程,可在多線程裡面呢?

上面就是我們剛才結果錯亂得原因,也就是說:我們計算和指派是兩部分,但是該多線程它沒有順序執行,這也就是我們所說的線程不安全。

因為,執行太快了,兩個線程互動交織在一起,最終得到我們這個錯誤結果。以上就是線程不安全的問題。

這就是需要 「lock 鎖」,給它上一把鎖,來達到我們 「number」 的效果,這個時候為了避免錯誤,我們要給他上一把鎖了。再給你講解上鎖之前呢,「接下來,我們來講一點複雜的例子:」

在一個程序中的「多個線程是共享資源的」

「比如」

在一個程序中,有一個全局變量 count 用來計數,現在我們聲明多個線程,每個線程運作時都給 count 加 1,讓我們來看看效果如何,代碼實作如下:

在這裡,我們聲明了 1000 個線程,每個線程都是現取到目前的全局變量 count 值,然後休眠一小段時間,然後對 count 賦予新的值。

那這樣,按照常理來說,最終的 count 值應該為 1000。但其實不然,我們來運作一下看看。

運作結果如下:

最後的結果居然隻有 69,而且多次運作或者換個環境運作結果是不同的。

「這是為什麼呢?」

因為 count 這個值是共享的,每個線程都可以在執行 temp = count 這行代碼時拿到目前 count 的值,但是這些線程中的一些線程可能是并發或者并行執行的,這就導緻不同的線程拿到的可能是同一個 count 值,最後導緻有些線程的 count 的加 1 操作并沒有生效,導緻最後的結果偏小。

是以,如果多個線程同時對某個資料進行讀取或修改,就會出現不可預料的結果。為了避免這種情況,我們需要對多個線程進行同步,要實作同步,我們可以對需要操作的資料進行加鎖保護,這裡就需要用到 threading.lock 了。

「加鎖保護是什麼意思呢?」

就是說,某個線程在對資料進行操作前,需要先加鎖,這樣其他的線程發現被加鎖了之後,就無法繼續向下執行,會一直等待鎖被釋放,隻有加鎖的線程把鎖釋放了,其他的線程才能繼續加鎖并對資料做修改,修改完了再釋放鎖。這樣可以確定同一時間隻有一個線程操作資料,多個線程不會再同時讀取和修改同一個資料,這樣最後的運作結果就是對的了。

我們可以将代碼修改為如下内容:

示例一的修改:

在代碼:「lock.acquire() 與 lock.release()」 中間的這個過程讓它強制有這個計算和指派的過程,也就是讓他執行完這兩個操作,後再切換。這樣就不會完成計算後,還沒來的及指派就跑到下一個去了。這樣也就防止了線程不安全的情況。

然後,就是我們第一個線程拿到這把鎖的 「lock.acquire()」 了,那另一個線程就會在 「lock.acquire()」 阻塞了,直到我們另一個線程把 「lock.release()」 鎖釋放,然後拿到鎖執行,就這樣不斷地切換拿鎖執行。

**死鎖:**就是前面的線程拿到鎖之後,運作完卻不釋放鎖,下一個線程在等待前一個線程釋放鎖,這種就是死鎖。說的直白一點就是,互相等待。就像照鏡子一樣,你中有我,我中有你。也就是在沒有 release 的這種情況。(你等我表白,我等你表白)

「示例二的加鎖」

在這裡我們聲明了一個 lock 對象,其實就是 threading.lock 的一個執行個體,然後在 run 方法裡面,擷取 count 前先加鎖,修改完 count 之後再釋放鎖,這樣多個線程就不會同時擷取和修改 count 的值了。

這樣運作結果就正常了。

關于 python 多線程的内容,這裡暫且先介紹這些,關于 theading 更多的使用方法,如信号量、隊列等,可以參考官方文檔:https://docs.python.org/zh-cn/3.7/library/threading.html#module-threading。

再次複用,一個鎖可以再嵌套一個鎖。向我們上面的普通鎖,一個線程裡面,你隻能擷取一次。如果擷取第二次就會報錯。

遞歸鎖什麼時候用呢?需要更低精度的,力度更小,為了更小的力度。

我們會發現這個遞歸鎖是比較耗費時間的,也就死我們擷取鎖與釋放鎖都是進行上下文切換導緻資源消耗的,是以說開啟的鎖越多,所耗費的資源也就越多,程式的運作速度也就越慢。一些大的工程很少上這麼多的鎖,因為這個鎖的速度會拖慢你整個程式的運作速度。是以得思考好,用不用這些東西。

05-7 萬字長文:實作多線程(下)
05-7 萬字長文:實作多線程(下)
05-7 萬字長文:實作多線程(下)

繼續閱讀