The Little Book of Semaphores 訊號量小書 第八章 Python中的同步
第八章 Python中的同步
通過使用虛擬碼,我們避免了現實世界中一些醜陋的同步細節。 在本章中,我們將介紹Python中的實際同步程式碼; 在下一章我們將看看C.
Python提供了一個相當令人愉快的多執行緒環境,並配有Semaphore物件。 它有一些缺點,但附錄A中有一些清理程式碼可以使事情變得更好。
這是一個簡單的例子:
第一行執行附錄A中的清理程式碼; 我將從其他例子中刪除這一行。
Shared定義了一個包含共享變數的物件型別。 全域性變數也線上程之間共享,但我們不會在這些示例中使用任何變數。 在函式內部宣告的執行緒也是區域性的,因為它們是特定於執行緒的。
子執行緒的程式碼是一個無限迴圈,遞增計數器,列印新值,然後睡眠0.5秒。
父執行緒建立shared變數和兩個子執行緒,然後等待子執行緒退出(在這種情況下,它們不會退出)。
8.1 互斥鎖檢查問題
關注同步問題的勤奮學生會注意到子執行緒對計數器進行了不同步的更新,這是不安全的! 如果您執行此程式,您可能會看到一些錯誤,也可能看不到。 同步錯誤的可怕之處是它們是不可預測的,這意味著即使廣泛的測試也可能無法發現它們。
為了檢測錯誤,經常需要自動化搜尋。 在這種情況下,我們可以通過跟蹤計數器的值來檢測錯誤。
在此示例中,shared包含一個列表list(誤導性命名為陣列array),用於跟蹤計數器的每個值的使用次數。 每次迴圈時,子執行緒都會檢查counter計數器,如果超過end則退出。 如果不是,則使用counter作為陣列的索引並遞增相應的條目。 然後他們遞增counter。
如果一切正常,陣列中的每個條目應該只增加一次。 當子執行緒退出時,父節點從join返回並列印陣列的值。當執行這個程式時,我得到:
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
這是令人失望的正確。 如果我們增加陣列的大小,我們可能會期望更多的錯誤,但是檢查結果也變得更加困難。
我們可以通過對陣列的結果生成直方圖來自動檢查:
現在,執行程式後,我得到:
{1:10}
這意味著值1出現了10次,如預期的那樣。 到目前為止沒有任何錯誤,但如果我們把end變得更大,事情變得更有趣:
end = 100, {1: 100} end = 1000, {1: 1000} end = 10000, {1: 10000} end = 100000, {1: 27561, 2: 72439}
哎呀! 當end足夠時,子執行緒之間有很多上下文切換,就開始出現同步錯誤。 在這種情況下,我們會遇到很多錯誤,這表明程式陷入了迴圈模式,其中執行緒在臨界區中一直被中斷。
這個例子演示了同步錯誤的一個危險,即它們可能是罕見的,但它們不是隨機的。如果在一百萬次中出現一次錯誤,那並不意味著它不會連續發生一百萬次。
思考:向此程式新增同步程式碼以強制獨佔訪問共享變數。 您可以從greenteapress.com/semaphores/counter.py下載本節中的程式碼。
8.1.1 互斥鎖檢查提示
以下是我使用Shared變數的版本:
唯一的變化就是名為mutex的訊號量,這應該不足為奇。
8.1.2 互斥鎖檢查方案
以下是我的解決方案:
雖然這不是本書中最難的同步問題,但是您可能已經發現要正確處理細節非常棘手。特別是,很容易忘記在中斷迴圈之前發出互斥鎖訊號,這會導致死鎖。
我使用end = 1000000執行此解決方案,並得到以下結果:
{1:1000000}
當然,這並不意味著我的解決方案是正確的,但它是一個良好的開端。
8.2 可樂機問題
下面的程式模擬生產者和消費者從可樂機中新增和移除可樂:
機器的容量是10瓶,而機器最初是半滿的。 所以共享變數cokes是5。
該程式建立4個執行緒,兩個生產者和兩個消費者。它們都執行迴圈loop,但生產者呼叫produce,消費者呼叫consume。這些函式對共享變數進行了不同步的訪問,這是禁止的。
每次通過迴圈,生產者和消費者睡眠一段時間,該時間從mu的指數分佈中選擇。由於有兩個生產者和兩個消費者,平均每秒可以將兩個可樂新增到機器中,並且有兩個可樂被移除。
因此,焦炭的平均數是恆定的,但在短期內可以變化很大。如果執行該程式一段時間,您可能會看到cokes的數值跌到零以下,或爬升到10以上。當然,這兩種情況都不應該發生。
思考:為此程式新增程式碼以強制執行以下同步約束:
- 訪問cokes應該是互斥的。
- 如果cokes的數值是零,消費者應該阻塞,直到添加了可樂。
- 如果cokes的數值是10,生產者應該阻塞,直到可樂被移除。
您可以從greenteapress.com/semaphores/coke.py下載該程式。
8.2.1 可樂機提示
以下是我在解決方案中使用的共享變數:
cokes現在是一個訊號量(而不是一個簡單的整數),這使得列印它的價值變得棘手。 當然,你永遠不應該訪問訊號量的值,並且Python以其通常的do-gooder方式並不提供你在某些實現中看到的任何欺騙方法。
但您可能會發現,將訊號量的值儲存在一個名為_Semaphore__value的私有屬性中是很有意思的。另外,在你不知道的情況下,Python實際上不會對訪問私有屬性實施任何限制。當然,你永遠不應該訪問它,但我認為你可能會感興趣。
啊咳。
8.2.2 可樂機方案
如果您已閱讀本書的其餘部分,那麼您應該毫不費力地提出至少與此類似的內容:
如果您執行這個程式一段時間,您應該能夠確認機器中的可樂數量從不是負數或大於10。所以這個解決方案似乎是正確的。 到目前為止。