Java:使用Java線程的并行處理
本文以Java線程為例介紹并行處理。所討論的許多挑戰(zhàn)也適用于其他編程語言,如C++或C#。
一目了然
并行編程技術是有效利用多核系統(tǒng)(多核處理器、圖形卡或HPC集群)所必需的。
內(nèi)存模型的知識對于開發(fā)并行應用程序至關重要。
不同步的并行內(nèi)存訪問可能導致錯誤的結果和程序中止。
線程是應用程序(進程)中的順序執(zhí)行字符串。應用程序可以由多個并行運行的線程組成。應用程序中的線程共享啟動它們的進程的內(nèi)存(共享內(nèi)存)。
為了創(chuàng)建線程,Java和許多其他語言一樣,提供了thread類。如果開發(fā)人員想要創(chuàng)建自己的線程,他們會創(chuàng)建一個從線程派生的類,該類重寫run方法。它包含線程在運行時要執(zhí)行的程序代碼。啟動線程時,首先創(chuàng)建類的實例,然后調(diào)用Start方法?;蛘?,也可以通過實現(xiàn)Runnable接口來創(chuàng)建線程。因此,類不必從線程派生,這為更復雜的程序結構提供了優(yōu)勢,因為Java不支持多重繼承。
避免數(shù)據(jù)泄露
如果開發(fā)人員反復運行程序,程序偶爾會中止。觸發(fā)是所謂的數(shù)據(jù)競賽,即多個線程訪問同一數(shù)據(jù)(在本例中為sum變量)并嘗試更改數(shù)據(jù)的星座。sum變量被實現(xiàn)為64位數(shù)據(jù)類型(double)。Java的內(nèi)存模型將64位數(shù)據(jù)類型實現(xiàn)為非原子的,也就是說,不是線程安全的,因為值的寫入操作分兩步進行,每32位的一半寫入一個步驟。這可能會導致一個線程讀取64位值的狀態(tài),其中前32位值已經(jīng)被另一個線程更改,但后半部分尚未更改(另請參見Java語言規(guī)范[1])。這可以通過將變量標記為易失性來解決。對于標記為volatile(例如double和long)的64位變量,Java運行時環(huán)境確保寫入始終是線程安全的,并且其他線程只能看到完全寫入的值(在兩個32位半部分上)。
但是,程式的結果會在每次執(zhí)行程式后顯示不同的結果。但未列出1億的正確結果。原因是求和的計算:這是一種所謂的種族條件,也就是說,結果取決于線程執(zhí)行的時間順序??偤陀嬎阌扇齻€部分組成:讀取舊總和,將總和與數(shù)組中的相應值相加,然后保存新總和。在多個線程上執(zhí)行這些操作的時間順序是隨機的,不是確定性的。

關鍵部分同步
為了解決求和計算中的種族條件問題,Java提供了將方法標記為已同步的方法。這可確保每次只能由一個線程運行它們。所有其他線程都必須等待某個執(zhí)行者再次退出該方法。
如果程序再次運行,則會返回可重復的正確結果。但是,兩個線程的運行時間大約為5000 ms,此示例說明了同步會對性能造成很大的影響,因為計算總和時所需的邏輯會變得復雜得多。調(diào)用同步方法會在對象上設置鎖,或者在退出該方法時重置鎖。如果方法已經(jīng)鎖定,則調(diào)用線程將被阻塞。因此,除了實際的總和計算之外,本示例還調(diào)用此同步機制1億次。
在這些條件下,并行計算運行
多核心系統(tǒng)(多核心處理器、顯示卡或高效能運算(HPC)群集)需要平行程式設計技術。這可以顯著提高性能并縮短復雜應用程序的運行時間。該Java線程示例說明了并行應用程序開發(fā)的基本挑戰(zhàn),并可應用于其他語言,如C++和C#以及圖形卡并行處理等技術?;驹O置始終相同:
該問題必須很好地并行,并且具有最小的順序(非并行)部分。適用于數(shù)值模擬方法、神經(jīng)網(wǎng)絡訓練以及矩陣、張量和向量計算等數(shù)學方法。
要開發(fā)并行應用程序,了解存儲模型至關重要,例如數(shù)據(jù)類型的原子性或可見性(變量在多個線程中的變化是可見的)。
不同步、并行的內(nèi)存訪問可能導致錯誤的結果和程序中斷。但任何形式的同步(原子變量,∞成本高昂,降低了并行計算的效率。此外,對程序代碼的生成情況的了解要差得多。
應盡量減少共享可變數(shù)據(jù)。最佳選擇是每個線程單獨訪問內(nèi)存,或?qū)Χ鄠€線程的數(shù)據(jù)進行只讀訪問。