對多核處理器進(jìn)行編程以利用其強大功能意味著編寫多線程代碼。C 和 C++ 不是為并發(fā)而設(shè)計的,因此開發(fā)人員必須為這些語言使用諸如 pthreads 之類的庫。由于全新類別的編程缺陷帶來的風(fēng)險,多線程代碼比單線程代碼更難正確處理。
在流氓的并發(fā)錯誤庫中,競爭條件是臭名昭著的屢犯者。競爭條件發(fā)生在程序檢查資源屬性并假設(shè)該屬性沒有更改的情況下執(zhí)行操作,即使外部參與者已經(jīng)介入并更改了該屬性。
數(shù)據(jù)競爭是一種特殊類型的競爭條件,它涉及對多線程程序中內(nèi)存位置的并發(fā)訪問。當(dāng)有兩個或多個執(zhí)行線程訪問共享內(nèi)存位置,至少一個線程正在更改該位置的數(shù)據(jù),并且沒有明確的協(xié)調(diào)訪問機制時,就會出現(xiàn)此缺陷。如果發(fā)生數(shù)據(jù)競爭,它會使程序處于不一致的狀態(tài)。
數(shù)據(jù)競爭的陰險本質(zhì)
人們普遍認(rèn)為,一些數(shù)據(jù)競爭是無害的,可以安全地忽略。不幸的是,這僅在極少數(shù)情況下是正確的。最好通過舉例說明原因。
單例模式是一種常見的習(xí)慣用法,其中程序維護(hù)對單個底層對象的引用,如果已初始化,則布爾變量對其進(jìn)行編碼。這種模式也稱為延遲初始化。以下代碼是該模式的示例:
if (!initialized) {
object = create();
initialized = true;
}
。。. 目的 。。.
這段代碼完全適合單線程程序,但它不是線程安全的,因為它在名為initialized的變量上存在數(shù)據(jù)競爭。如果由兩個不同的線程調(diào)用,則存在兩個線程幾乎同時觀察到初始化為 false 的風(fēng)險,并且都將調(diào)用create(),從而違反了單例屬性。
為了使這個線程安全,自然的方法是用鎖保護(hù)整個if語句。然而,獲取和釋放鎖的成本可能很高,因此程序員試圖通過使用雙重檢查鎖定習(xí)慣用法來避免這種成本——在鎖范圍之外進(jìn)行檢查,在鎖范圍內(nèi)進(jìn)行檢查。內(nèi)部檢查用于確認(rèn)在獲得鎖后第一個檢查仍然有效:
if (!initialized) {
lock();
if (!initialized) {
object = create();
initialized = true;
}
unlock();
}
。。. 目的 。。.
從表面上看,這看起來就足夠了,實際上,只要保證語句按該順序執(zhí)行就足夠了。但是,優(yōu)化編譯器可能會生成實質(zhì)上切換object = create()和initialized = true順序的代碼。畢竟,這兩個語句之間沒有明確的依賴關(guān)系。在這種情況下,如果第二個線程在分配給initialized之后的任何時間進(jìn)入此代碼,則該線程將在object被初始化之前使用它的值。
優(yōu)化編譯器是不可思議的野獸。那些優(yōu)化速度的人會考慮許多深奧的考慮,其中很少有對程序員來說是顯而易見的。他們通常會生成明顯無序的指令,因為這樣做可能會導(dǎo)致更少的高速緩存未命中,或者因為需要更少的指令。
假設(shè)因為重新排序在前面的示例中引入了競爭條件,所以認(rèn)為編譯器有問題是錯誤的。編譯器正在做它被允許做的事情。語言規(guī)范對此非常清楚和明確:允許編譯器假設(shè)程序中沒有數(shù)據(jù)競爭。
實際上,規(guī)范更廣泛:允許編譯器在存在未定義行為的情況下做任何事情。這有時被開玩笑地稱為著火語義;如果程序具有未定義的行為,該規(guī)范允許編譯器將計算機置于火上。除了數(shù)據(jù)競爭之外,緩沖區(qū)溢出、無效地址的取消引用等許多傳統(tǒng)錯誤也構(gòu)成了未定義的行為。因為編譯器可以自由地做任何事情,而不是燒毀建筑物,他們通常會做明智的事情,即假設(shè)未定義的行為永遠(yuǎn)不會發(fā)生并相應(yīng)地進(jìn)行優(yōu)化。
即使對于并發(fā)和編譯器方面的專家來說,這樣做的后果有時也會令人驚訝。很難讓程序員相信看起來完全正確的代碼可以編譯成有嚴(yán)重錯誤的代碼。
另一個例子是值得描述的。假設(shè)有兩個線程,一個讀取共享變量,另一個寫入共享變量。讓我們假設(shè)讀者在寫入者更改之前或之后看到該值并不重要(這不是一種不常見的模式)。如果這些訪問不受鎖保護(hù),那么顯然存在數(shù)據(jù)競爭。然而,盡管著火規(guī)則,大多數(shù)程序員會得出結(jié)論,這是完全良性的。
事實證明,至少有兩種合理的方式可以編譯這段代碼,讀者會看到錯誤的值。第一種方法很容易解釋:假設(shè)該值是一個只能讀取 32 位字的架構(gòu)上的 64 位數(shù)量。那么讀者和作者都需要兩條指令,不幸的交錯可能意味著讀者看到舊值的前 32 位和新值的后 32 位,當(dāng)它們組合時可能不是舊值也不是新的。
生成錯誤代碼的第二種方式更為微妙。假設(shè)讀者做了以下事情,其中??數(shù)據(jù)競爭在名為global的變量上:
int local = global; // Take a copy of
// the global
if (local == something) {
。..
}
。.. // Some non-trivial code that does
// not change global or local
if (local == something) {
。..
}
在這里,讀者正在制作 racy 變量的本地副本并引用該值兩次??梢院侠淼仄谕麅蓚€地方的值相同,但同樣,優(yōu)化編譯器可以生成未滿足期望的代碼。如果將local分配給一個寄存器,那么它將有一個值用于第一次比較,但如果兩個條件之間的代碼足夠重要,那么該寄存器可能會溢出——換句話說,為了不同的目的而重用。在這種情況下,在第二個條件下,local的值將從全局變量重新加載到寄存器中,此時編寫器可能已將其更改為不同的值。
程序員應(yīng)該非常懷疑某些數(shù)據(jù)競爭是可以接受的,并且應(yīng)該努力從他們的代碼中找到并刪除它們。
發(fā)現(xiàn)風(fēng)險缺陷的技術(shù)
在發(fā)現(xiàn)并發(fā)缺陷時,傳統(tǒng)的動態(tài)測試技術(shù)可能不夠用。一個通過一百次測試的程序并不能保證下一次通過,即使是相同的輸入和相同的環(huán)境。這些錯誤是否出現(xiàn)對時間非常敏感,線程中的操作交錯的順序本質(zhì)上是不確定的。
用于發(fā)現(xiàn)數(shù)據(jù)競爭的新動態(tài)測試技術(shù)正在出現(xiàn)。這些技術(shù)通過在應(yīng)用程序執(zhí)行時監(jiān)視它們并觀察每個線程持有的鎖以及這些線程正在訪問的內(nèi)存位置來工作。如果發(fā)現(xiàn)異常,則發(fā)出診斷。其他工具有助于診斷可能導(dǎo)致故障的數(shù)據(jù)競爭。一些公司現(xiàn)在提供工具來促進(jìn)數(shù)據(jù)競爭的診斷,從而允許重播導(dǎo)致異常的事件。
靜態(tài)分析工具也可用于查找數(shù)據(jù)競爭和其他并發(fā)錯誤。動態(tài)測試工具會發(fā)現(xiàn)針對具有固定輸入集的程序的特定執(zhí)行出現(xiàn)的缺陷,而靜態(tài)分析工具會檢查所有可能的執(zhí)行和所有可能的輸入。出于性能原因,工具可能會限制進(jìn)行多少探索,因此可能并不完全詳盡;即便如此,它們可以涵蓋的范圍遠(yuǎn)遠(yuǎn)超過動態(tài)測試所能實現(xiàn)的范圍。靜態(tài)分析的優(yōu)點是不需要測試用例,因為程序從未真正執(zhí)行過。
相反,這些工具通過創(chuàng)建程序模型然后以各種方式探索模型以發(fā)現(xiàn)異常來工作。GrammaTech 的 CodeSonar 通過創(chuàng)建表示每個線程持有的鎖集的模型并通過執(zhí)行探索執(zhí)行路徑的程序的符號執(zhí)行來發(fā)現(xiàn)數(shù)據(jù)競爭。它記錄受鎖保護(hù)的變量集,并使用此信息來查找可能導(dǎo)致共享變量在沒有適當(dāng)同步的情況下使用的交錯。類似的技術(shù)可用于發(fā)現(xiàn)其他并發(fā)缺陷,例如死鎖和鎖管理不善。
一旦發(fā)現(xiàn),數(shù)據(jù)競爭通常很容易修復(fù),盡管這樣做會導(dǎo)致性能損失。在某些情況下,可能會嘗試使用 C 中的 volatile 關(guān)鍵字來糾正數(shù)據(jù)爭用,但不建議這樣做,因為 volatile 并非旨在解決并發(fā)問題,并且在任何情況下都是一個難以理解的構(gòu)造,經(jīng)常被錯誤編譯。最新版本的 C 和 C++ 包含并發(fā)并支持原子操作。對這些操作的編譯器支持正在慢慢出現(xiàn),在它變得可用之前,最好的方法是使用鎖。
為了實現(xiàn)多核處理器的高質(zhì)量軟件,建議對數(shù)據(jù)競爭采取零容忍政策。使用靜態(tài)和動態(tài)技術(shù)的組合來查找它們,并注意不要過度依賴深奧的編譯器技術(shù)來修復(fù)它們。這些缺陷是如此危險和不可預(yù)測,因此系統(tǒng)地消除它們是確保它們不會造成傷害的唯一安全方法。
審核編輯:郭婷
-
處理器
+關(guān)注
關(guān)注
68文章
19896瀏覽量
235268 -
C++
+關(guān)注
關(guān)注
22文章
2119瀏覽量
75324 -
編譯器
+關(guān)注
關(guān)注
1文章
1662瀏覽量
50228
發(fā)布評論請先 登錄
MBSE工具+架構(gòu)建模:從效率提升到質(zhì)量賦能

您的模型診斷專家MI:助力把好模型質(zhì)量關(guān)

CADENAS 數(shù)字產(chǎn)品配置器輕松實現(xiàn)Ascendor電梯規(guī)劃
異形拼接處理器可以實現(xiàn)的效果

廣汽集團(tuán)召開高質(zhì)量發(fā)展大會
QorIQ?T1042多核處理器
借助谷歌Gemini和Imagen模型生成高質(zhì)量圖像

旋智多核心處理器助力電機控制應(yīng)用

信雅達(dá)入選2024年度中國軟件高質(zhì)量發(fā)展前百家企業(yè)
中國算力大會召開,業(yè)界首個算力高質(zhì)量評估體系發(fā)布

知識分享 | 輕松實現(xiàn)優(yōu)質(zhì)建模

評論