擰龍頭法測試併發程式
一個執行緒解決不了問題,就讓兩個執行緒來;兩個執行緒沒辦法,就上三個執行緒。 三個以上執行緒能夠解決的問題,遲早會帶來新問題 。我們還真就不信那麼多併發程式都寫對了……
併發bug:你怎麼還不出現,我等得花兒也謝了
在生活中,如果我們一個人解決問題太慢,那麼就讓多一些人來一起解決,這和併發程式(或稱為多執行緒程式)的初衷一致---多個執行緒共同協調好解決問題。但我們知道,一旦參與者變多/執行緒增多,那麼整個處理過程就可能失控。 具體來看,我們有一個炒雞簡單的概念程式,分別對共享變數 X 寫入 0/1 兩個不同值,然後執行緒 1 在結束前做一個斷言。

結果顯而易見,這個執行結果可能會正常退出,也可能觸發斷言失敗。
X=0 => X=1 => Assert(x==1) 執行結果正常 X=1 => X=0 => Assert(x==1) 觸發斷言失敗
所以我們可以看到併發程式的一個特點: 執行的不確定行 。
不要覺得這個例子太簡單很扯淡。不止一個知名的開源軟體裡有把指標設定成NULL (相當於 X=0
),然後被另一個執行緒解引用的事情發生,這都是血淋淋的真實案例。

不是 bug 躲著你,是那個 CPU,那場公平的排程;你握著了互斥鎖,而她卻在等訊號量。
這種不確定性會導致什麼問題呢?想必你已經發現了(或者你經歷過被支配的恐懼)那就是---在除錯程式的時候,你期待的 bug,不會輕易出現,往往你需要執行“好多好多好多好多很多很多很多”次才能觸發—在測試環境下也許多到海枯石爛宇宙坍縮恐怕都不會出現,但一旦部署沒幾天伺服器就爆炸了。

正如我們上面的程式,程式執行觸發 bug 需要特定的排程:“執行緒 2 需要在斷言之前寫入 X=0”,而因為程式中還有其他很多很多的語句需要執行,在實際的排程中,可能這個觸發 bug 的排程是低概率出現的。這樣的 bug 甚至有一個專有名詞“heisinbug” [1],可以說很形象了,和薛定諤的貓一樣,除非最後觀測,不然無法知道那個 bug 能否觸發。
這種時而出現,時而消失的 bug 是及其惱人的,好比看到一個美女,想著再次偶遇,於是在那條路上來來回回地重複走,可是那個姑娘卻不再出現,即使等的花兒都謝了,人家還是不出現:sob:。雖然我們的工作無法讓你偶遇美女,但卻能幫助你大大減少併發帶來的痛苦,這麼看來,就可以更加愉快的寫程式碼了,還需要什麼姑娘(手動:dog:)。

執行緒提效能,併發容易掛;不信你去寫,bug 饒過誰。編寫、測試、除錯併發程式是苦痛的經歷,求不得,遇不見,只能祈禱;一般而言,併發搞習慣了,心態也就變得佛繫了:blush:。
從除錯程式碼的角度看這一切是折磨人的,那麼從另一個方面---“測試”來說,這也是惱人的小妖精。我們看到觸發 bug 需要很多的嘗試,但我們的測試受限於一定的時間,所以對於併發程式而言,我們就可能測試不充分,從而讓 bug 上線,造成災難 [2, 3],想想都嚇得要吃手手:fearful:。

總體而言,我們面臨的一個問題就是: 如何有效的測試併發程式,在一定的時間內,儘可能找出 bug 呢? 迎著這個問題,下面我們簡單介紹一下我們針對併發程式展開的一份工作。
找出併發bug:一個新思路
從問題出發,構造一個解決方案吧。
在測試工作中,一個重要的性質就是多樣性,我們希望我們總是能夠儘可能地讓程式執行到不同的狀態上;而考慮到併發程式的結果依賴於排程,所以我們強調的是能夠嘗試不同的排程。 現在我們思考下,為什麼多個執行緒執行就有不確定性呢?答案很容易想到,作業系統對於執行緒的排程是不確定的。那麼基於作業系統的執行緒排程,給我們的測試多樣性會帶來怎樣的影響呢? 為了回答這個問題,讓我們簡單回顧下大學課本的內容,作業系統一般是如何進行的呢?“公平性”!一般而言作業系統會公平地排程各個執行緒,讓他們分配到儘可能相同的時間片來佔有 CPU 執行程式碼。如果是這樣的排程,那麼我們可以想象,在多次的測試中,每個執行緒執行的軌跡會比較相似,這不能達到我們期待的測試多樣性。 那麼如何更好地測試呢?打破已有的公平性!讓不同執行緒能佔有不同的時間片進行執行,這就類似,不同的執行緒執行的速度有快慢,有的執行緒執行地更快了(分配了更多的時間執行),有的執行緒執行地更慢了(獲得更少執行時間)。所以我們提出一個概念叫做 執行緒執行速度 。打比方來說,每個執行緒好比是水龍頭,我們現在試圖去擰水龍頭,讓不同的龍頭有不同的出水速度。

我們看一個具體的例子,右圖中表明執行緒 1 執行的綠圈事件在一般先於執行緒 2 的藍圈事件;而當我們調節這兩個執行緒的執行速度,執行緒 1 執行得更慢,執行緒 2 執行的更快時候,藍圈事件能夠被更早地執行到。形象地,我們可以從左圖中看到藍圈能夠發生在綠圈之前啦! 這看似是一個非常簡單的方法,但卻很有效,它幫助我們打破了作業系統排程的公平性,探索了更多不同的排程---這就構成了我們工作的初衷。當然,如何去“擰水龍頭”又是一個問題,我們需要一個系統的方法來達成控制執行緒執行速度的調節。

關於“速度調節”的理論(可以無視)
看待一樣東西我們總可以從不同的角度入手,剛才我們從“測試多樣性+作業系統排程”的視角引入了速度調節,那麼現在我們從併發程式排程本身來看下執行速度的性質。 對於一個併發程式,我們假設它由三個執行緒 構成,並且考慮最簡單的情況---三個執行緒的執行不相互依賴。那麼從程式執行開始,我們不斷選擇一個執行緒執行一步直到程式結束。我們可以有不同的執行序列:
這樣線性的序列不夠直觀,我們可以用一棵 排程樹 在把它們都呈現出來,每一個節點就是程式到達的一個狀態,每個狀態下都有三條邊,表明選擇哪一個執行緒執行一步到達下一個狀態。這棵排程樹其實就是程式的排程空間。

現在,給每個執行緒一個 數值 作為它的執行速度,這樣所有執行緒的執行速度就構成了一個 速度向量 ,以三個執行緒為例,就是 ;而這些速度向量組成了整個 速度空間 ,對於三個執行緒的話,這就是一個立方體,而對於更多的執行緒數,就是一個超立方體(我們限制了速度
的取值範圍)。這樣從樹狀的排程空間轉換到了速度空間有什麼好處呢?我們這個速度空間是一個數值空間,所以我們可以方便地在裡面進行系統地取樣速度向量。 還記得我們受限於有限的測試時間/資源的前提嗎,所以即使我們有了速度空間,我們仍然不能遍歷整個空間/嘗試所有的速度向量,所以我們只能在空間裡取樣部分的速度向量,也就是說在超立方體裡面對點進行取樣。那麼我們是如何進行系統地取樣呢?

我們在引入執行緒速度的時候談到,在大部分情況下,作業系統總是排程執行緒以近乎相同的執行速度,而在速度空間中,這些速度向量分佈在了超立方體的對角線周圍。視覺化地來看,以兩個執行緒組成的二維平面速度空間為例,這些“正常速度”就在對角線附近。而我們自然地會想到,那麼在空間邊緣的點是如何的,我們稱這些點代表的速度為“極端速度”,因為他們以為著我們控制一個執行緒以特別塊/慢的速度進行執行,而這在普通的執行中是難以出現的。
調節速度的演算法(可以無視)

我們的目的就是取樣 極端 的執行速度,首先我們需要兩個極端值,代表了最快和最慢執行速度。然後依次控制執行緒對(兩個執行緒):其中一個執行緒的速度為極端速度,另一個執行緒迭代地以指數方式嘗試速度,而對於其他速度,我們讓其隨機。 那麼我們這麼做的物理意義是什麼呢?首先我們固定了一個執行緒的速度為極端值,表明了我們在超立方體中找了一個超平面,然後我們在這個超平面上系統地隨機取樣,如下圖所示。

使用“速度調節”測試多執行緒程式
我們通過執行速度這個概念,將執行緒排程空間轉換到了速度空間,然後提出一種簡單/有針對性的取樣方法在這個空間裡取樣,當然最後我們還需要一個實際的排程器,來按照速度向量的值對各個執行緒進行排程,這就是一個實現問題啦。
DONE!

如果對一些細節感興趣,可以從我們的文章 [5] 中瞭解更多。
實驗結果
我們把這一堆簡單的、複雜的東西集中在一起,實現了一個原型工具叫 Schnauzer(我不會告訴你們為何取這個名字),然後選擇了 6 個紅紅火火的開源程式作為實驗物件,和現有工作 PCT [4] 進行比較。 那麼我們是如何比較兩份工作的呢?我們其實對比的是,在不同的工具下,每次排程之後,能夠在執行中檢測到的資料競爭數目。 所謂資料競爭就是“兩個及以上執行緒同時訪問一個記憶體變數,但沒有同步好這兩個執行緒,並且其中一個訪問操作是寫操作”。可以想象,資料競爭的存在很有可能會導致併發缺陷,它是併發缺陷的一個強有力指示,所以一般大家都力圖檢測到更多的資料競爭。(悄悄說一句,但有些資料競爭其實並不會導致缺陷,例如我們實現一個 spin lock 勢必會引入競爭,那麼如何判別資料競爭的好壞又是一個問題)。 實驗結果自然是恍恍惚惚的,我們能夠比 PCT 在大多數的實驗物件上有更優的表現。

光給這種實驗資料實在太乏味了,我們還做了點有意思的工作,例如在 Transmission 和 Cherokee 的最新版本中真的找到了未知的併發缺陷,並且我們也做了上報。考慮到這些程式已經被大家“玩”了太多次,我們能夠找到那些隱藏的缺陷,真是一顆賽艇的。每天都用的開源軟體能幫他們找到bug修復,對碼農來說是個很不錯的褒獎,而 似乎這才是整份工作真正讓人著迷的地方 。

上圖是 Transmission 中找到的一個缺陷,左圖是正常執行下的不出錯結果;右圖是我們在調節速度之下探索到的新排程,它能夠觸發一個 Use-After-Free bug。因為這個 bug 需要三個執行緒參與,涉及到五個事件,要檢測到這個 bug 還是不容易的呢。

你能忍受囉嗦看到這裡,說明你對我們的科研是感興趣的。既然如此,我們也不得不展露我們的誠意。
生硬的招生廣告(選看)
你對程式分析感興趣嗎? → 我們就是做這個的,專門“助人為樂、解放程式猿的壓力”,幫程式設計師又快又好地寫出程式; 你對編寫硬核程式碼痴迷嗎? → 作業系統、程式合成、編譯器,你想要的浪漫我們都有; 大佬老闆、基友師兄、小師妹,你期待的氛圍盡在這裡。Play with us!
接地氣的廣告(必看)
我們組裡人都超 nice 的,老闆人好不給壓力, 師兄師弟日常面基,師妹。。。有點少:cry:。 如果你對寫程式碼有興趣、對程式分析有感覺,歡迎加入。 我們不是測試程式,我們是分析程式 。
那麼哪裡才能買得到呢? ofollow,noindex">我們是南京大學System & Program Analysis (SPAR) Group 。
參考文獻
- Heisenbug - Wikipedia
- Software Bug Contributed to Blackout
- Nasdaq's Facebook Glitch Came From Race Conditions
- Sebastian Burckhardt, Pravesh Kothari, Madanlal Musuvathi, and Santosh Nagarakatte. A Randomized Scheduler with Probabilistic Guarantees of Finding Bugs. ASPLOS'10
- Dongjie Chen, Yanyan Jiang, Chang Xu, Xiaoxing Ma, and Jian Lu. Testing Multithreaded Programs via Thread Speed Control. ESEC/FSE'18
作者簡介 :本文作者包括南京大學的博士生(打手)陳冬傑、蔣炎巖博士、許暢教授、馬曉星教授和呂建教授。