多線程下數據同步問題

以下文字摘自《Windows 併發編程指南》,版權歸原作者所有,僅供學習和研究參考。

對於一般性的數據競爭問題,解決方案之一就是將對共享狀態的併發訪問串行化。互斥是最常使用的一種技術,用來保證每次只有一個線程能夠執行那些容易發生併發問題的指令區域。這組需要被串行化的操作稱爲臨界區(Critical Region).

在當前的系統中可以通過多種方式來標識臨界域,例如編程語言中的關鍵字或者系統函數調用,其中包含了許多專門的術語,例如鎖(Lock)、互斥體(Mutex)、臨界區(Critical Section)、監視器(Monitor)、二值信號量(Binary Semaphore)、以及事務。在不同的方式之間都存在一些語義差異。然而,這些方式的效果卻是基本相同的。只要所有執行臨界域代碼的線程都按照一致的方式來訪問數據,那麼就可以避免發生數據競爭的問題。

通過臨界區的假如,我們之前討論的線程之間切換可能導致的數據不一致問題,有了新的變化。

T           t1                                        t2
0           t1(E):  EnterCriticalRegion()
1           t1(0):  MOV  EAX,[a]   #0
2                                                     t2(0): EnterCiriticalRegion();
3           t1(1):  INC,EAX        #1
4           t1(2):  MOV [a],EAX    #1
5           t1(L):  LeaveCriticalRegion();
6                                                     t2(0):  MOV  EAX,[a]    #1
7                                                     t2(1):  INC,EAX         #2
8                                                     t2(2):  MOV  [a],EAX    #3
9                                                     t2(L):  LeaveCriticalRegion();
在上面這個例子中,t2試圖在時刻2進入臨界域,但這個線程不能進入,因爲t1已經處於臨界域中,因此t2必須等到t1在時刻5離開時才能進入。這樣就確保了每次只能有一個線程執行遞增操作。

信號量1965年提出的概念,它被視作作爲臨界域思想的一種擴展。信號量支持多種複雜的數據同步模式,它允許一定數量的線程同時處於臨界域中。信號量的概念很簡單。信號量在被創建時將被賦予一個初始值,並且只要這個值大於0,那麼線程就可以在將這個值減1後進入臨界域,而無需等待其他線程退出。然而,如果這個值一旦爲0,那麼任何希望遞減這個信號量的線程都必須等待,直到有其他的線程釋放信號量並且遞增這個值使其大於0.

因此,臨界域(也稱爲互斥體)只是一種特殊的信號量,並且這個信號量的計數器爲0或者1,這也是爲什麼臨界域稱爲二值信號量的原因。

臨界域的使用方法

在任何情況下,對於臨界域來說都存在兩個對應的操作:一個操作是進入,而另一個操作是退出。這種語法會使人們誤以爲在整個程序只有一個臨界域,這通常是不對的。在實際的程序中,我們可能需要使用多個臨界域,分別保護不同的數據集。因此在程序的執行過程中經常需要實例化、管理、進入和離開臨界域。

想要進入臨界域1的線程並不會影響臨界域2,反之亦然。因此,我們必須確保所有線程在訪問特定的數據時都以一致的方式進入臨界域。爲了說明這種情況,假設有兩個獨立的CriticalRegion對象,每個對象都定義了Enter和Leave方法。如果這兩個線程試圖遞增一個共享變量s_a,那麼它們必須首先獲得同一個CriticalRegion對象。如果它們分別獲得了不同的臨界域,那麼並不能確保實現互斥行爲,並且在程序中將出現競爭問題。

下面就是一個存在這種問題的示例程序

static  int a;
static CriticalRegion  cr1, cr2;   //在其他地方被初始化
void f(){cr1.Enter(); s_a++; cr1.Leave();}
void g(){cr2.Enter(); s_a++; cr2.Leave();}
在這個示例中存在錯誤,因爲f獲得臨界域cr1,而g獲得臨界域cr2.但在這些臨界域之間沒有通過互斥來保護。如果一個線程執行f,而另一個線程併發地執行g,那麼我們將看到數據競爭問題。

粗粒度臨界域和細粒度臨界域

當前的臨界域機制都是以需要互斥行爲的代碼段來定義的,而不是以再代碼段中被訪問的數據來定義的。
1.粗粒度    通過一個鎖來保護某個子系統或者複合數據結構中的各個部分。這是使程序保持正確性的最簡單方式。只有一個鎖需要管理,並且只有一個鎖需要獲得和釋放:這種方式減少了在實現同步時需要的空間和時間,並且確定在臨界域中應該包含哪些代碼完成依賴於線程是否需要訪問某些大型的且易於識別的數據。我們只需要非常少的工作就可以確保程序的安全性。然而,這種過度保守的方法可能會由於僞共享(FALSE  Sharing) 問題而對程序的可伸縮性帶來負面影響。僞共享將不必要地阻止對某些數據的併發訪問,即通過對訪問增加一些不必要的保護來確保程序執行的正確性。

2.細粒度    爲了提高程序的可伸縮性,我們可以對每一部分數據(或者數據中的某些部分)都使用一個鎖,這使得多個線程能夠同時訪問不相交的數據部分。這種方式能夠減少或者消除僞共享,使線程能夠實現更高程序的併發以及更高的可伸縮性。這種方法缺點在於需要管理大量的鎖,並且當需要同時訪問多個數據結構時,可能需要獲取多個鎖,這將在空間和時間上都會增加複雜性。如果使用不當,這種策略還會導致死鎖問題。如果在多個數據結構之間存在着複雜的不變性關係,那麼要消除數據競爭問題就會變得更加困難。

沒有任何一種方法能夠適用於所有的情況。程序通常會將這兩種極端情況之間找到某種平衡並且將一些技術結合起來使用。但一條通用的經驗法則是,首先從粗粒度的鎖定開始以確保程序的正確性,然後再根據可伸縮性的需求來調整更細粒度的區域,通過這種方法將可以實現可維護性更好的,更易於理解的和錯誤更少的程序。


發佈了25 篇原創文章 · 獲贊 16 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章