在 Python 中測試競爭條件

笑虎發表於2015-04-15

當你有多個程式或執行緒訪問相同的資料時,競爭條件是一個威脅。本文探討了在發現競爭條件後如何測試它們。

Incrmnt

你在一個名為“Incrmnt”的火熱新創公司工作,該公司只做一件事情,並且做得比較好。

你展示一個全域性計數器和一個加號,使用者可以點選加號,此時計數器加一。這太簡單了,而且容易使人上癮。毫無疑問這就是接下來的大事情。

投資者們爭先恐後的進入了董事會,但你有一個大問題。

競爭條件

在你的內測中,Abraham和Belinda是如此的興奮,以至於每個人都點了100次加號按鈕。你的伺服器日誌顯示了200次請求,但計數器卻顯示為173。很明顯,有一些請求沒有被加上。

先將“Incrmnt變成了一坨屎”的新聞拋到腦後,你檢查下程式碼(本文用到的所有程式碼都能在Github上找到)。

你的Web伺服器使用多程式處理流量請求,所以這個函式能在不同的執行緒中同時執行。如果你沒掌握好時機,將會發生:

所以儘管增加了兩次計數,但最終只增加了1。

你知道你可以修改這個程式碼,變為執行緒安全的,但是在你那麼做之前,你還想寫一個測試證明競爭的存在。

重現競爭

在理想情況下,測試應該儘可能的重現上面的場景。競爭的關鍵因素是:

•兩個 get_count 呼叫必須在兩個 set_count 呼叫之前執行,從而使得兩個執行緒中的計數具有相同的值。

set_count 呼叫,什麼時候執行都沒關係,只要它們都在 get_count 呼叫之後即可。

簡單起見,我們試著重現這個巢狀的情形。這裡整 個Thread 2 在 Thread 1 的首個 get_count 呼叫之後執行:

before_after 是一個庫,它提供了幫助重現這種情形的工具。它可以在一個函式之前或之後插入任意程式碼。

before_after 依賴於 mock 庫,它用來補充一些功能。如果你不熟悉 mock,我建議閱讀一些優秀的文件。文件中特別重要的部分是 Where To Patch

我們希望,Thread 1 呼叫 get_count 後,執行全部的 Thread 2 ,之後恢復執行 Thread 1。

我們的測試程式碼如下:

在首次 get_count 呼叫之後,我們使用 before_after 的上下文管理器 after 來插入另外一個 increment 的呼叫。

在預設情況下,before_after只呼叫一次 after 函式。在這個特殊的情況下這是很有用的,因為否則的話堆疊會溢位(increment呼叫get_count,get_coun t也呼叫 increment,increment 又呼叫get_count…)。

這個測試失敗了,因為計數等於1,而不是2。現在我們有一個重現了競爭條件的失敗測試,一起來修復。

防止競爭

我們將要使用一個簡單的鎖機制來減緩競爭。這顯然不是理想的解決方案,更好的解決方法是使用原子更新進行資料儲存——但這種方法能更好地示範 before_after 在測試多執行緒應用程式上的作用。

在 incrmnt.py 中新增一個新函式:

它保證在同一時間只有一個執行緒對計數進行讀寫操作。如果一個執行緒試圖獲取鎖,而鎖被另外一個執行緒保持,將會引發 CouldNotLock 異常。

現在我們增加這樣一個測試:

現在在同一時間,就只有一個執行緒能夠增加計數了。

減緩競爭

我們這裡還有一個問題,通過上邊這種方式,如果兩個請求衝突,一個不會被登記。為了緩解這個問題,我們可以讓 increment 重新連結伺服器(有一個簡潔的方式,就是用類似 funcy retry 的東西):

當我們需要比這種方法提供的更大規模的操作時,可以將 increment 作為一個原子更新或事務轉移到我們的資料庫中,讓其在遠離我們的應用程式的地方承擔責任。

總結

Incrmnt 現在不存在競爭了,人們可以愉快地點選一整天,而不用擔心自己不被計算在內。

這是一個簡單的例子,但是 before_after 可以用於更復雜的競爭條件,以確保你的函式能正確地處理所有情形。能夠在單執行緒環境中測試和重現競爭條件是一個關鍵,它能讓你更確定你正在正確地處理競爭條件。

打賞支援我翻譯更多好文章,謝謝!

打賞譯者

打賞支援我翻譯更多好文章,謝謝!

在 Python 中測試競爭條件

相關文章