存在共享資源(共享一個文件,一塊內存等等)的時候,為了防止並發訪問時共享資源的數據不一致,引入了同步機制。
主要內容:
- 同步的概念
- 同步的方法-加鎖
- 死鎖
- 鎖的粒度
1. 同步的概念
了解同步之前,先了解另外2個概念:
- 臨界區 - 也稱為臨界段,就是訪問和操作共享數據的代碼段。
- 競爭條件 - 2個或2個以上線程在臨界區里同時執行的時候,就構成了競爭條件。
所謂同步,其實防止在臨界區中形成競爭條件。
如果臨界區里是原子操作(即整個操作完成前不會被打斷),那么自然就不會出競爭條件。
但在實際應用中,臨界區中的代碼往往不會那么簡單,所以為了保持同步,引入了鎖機制。
2. 同步的方法-加鎖
為了給臨界區加鎖,保證臨界區數據的同步,首先了解一下內核中哪些情況下會產生並發。
內核中造成競爭條件的原因:
競爭原因 |
說明 |
中斷 | 中斷隨時會發生,也就會隨時打斷當前執行的代碼。如果中斷和被打斷的代碼在相同的臨界區,就產生了競爭條件 |
軟中斷和tasklet | 軟中斷和tasklet也會隨時被內核喚醒執行,也會像中斷一樣打斷正在執行的代碼 |
內核搶占 | 內核具有搶占性,發生搶占時,如果搶占的線程和被搶占的線程在相同的臨界區,就產生了競爭條件 |
睡眠及用戶空間的同步 | 用戶進程睡眠后,調度程序會喚醒一個新的用戶進程,新的用戶進程和睡眠的進程可能在同一個臨界區中 |
對稱多處理 | 2個或多個處理器可以同時執行相同的代碼 |
為了在編寫內核代碼時避免出現競爭條件,在編寫代碼之前就要考慮好臨界區在哪,以及怎么加鎖。
在編寫完代碼后再加鎖是非常困難的,很可能還會導致部分代碼重寫。
編寫內核代碼時,時時記着下面這些問題:
- 這個數據是不是全局的?除了當前線程以外,其他線程能不能訪問它?
- 這個數據會不會在進程上下文或者中斷上下文中共享?它是不是要在兩個不同的中斷處理程序中共享?
- 進程在訪問數據時可不可能被搶占?被調度的新程序會不會訪問同一數據?
- 當前進程會不會睡眠(或者阻塞)在某些資源上,如果是,它會讓共享數據處於何種狀態?
- 怎樣防止數據失控?
- 如果這個函數又在另一個處理器上被調度將會發生什么?
3. 死鎖
死鎖就是所有線程都在相互等待釋放資源,導致誰也無法繼續執行下去。
下面一些簡單的規則可以幫助我們避免死鎖:
- 如果有多個鎖的話,盡量確保每個線程都是按相同的順序加鎖,按加鎖相反的順序解鎖。(即加鎖a->b->c,解鎖c->b->a)
- 防止發生飢餓。即設置一個超時時間,防止一直等待下去。
- 不要重復請求同一個鎖。
- 設計應力求簡單。加鎖的方案越復雜就越容易出現死鎖。
4. 鎖的粒度
在加鎖的時候,不僅要避免死鎖,還需要考慮加鎖的粒度。
鎖的粒度對系統的可擴展性有很大影響,在加鎖的時候,要考慮一下這個鎖是否會被多個線程頻繁的爭用。
如果鎖有可能會被頻繁爭用,就需要將鎖的粒度細化。
細化后的鎖在多處理器的情況下,性能會有所提升。
舉個例子說明一下:比如給一個鏈表加鎖,同時有A,B,C 3個線程頻繁訪問這個鏈表。
那么當A,B,C 3個線程同時訪問這個鏈表時,如果A獲得了鎖,那么B,C線程只能等待A釋放了鎖后才能訪問這個鏈表。
如果A,B,C 3個線程訪問的是這個鏈表的不同節點(比如A是修改節點listA,B是刪除節點listB,C是追加節點listC),
並且這3個節點不是連續的,那么3個線程同時運行是不會有問題的。
這種情況下就可以細化這個鎖,把加在鏈表上的鎖去掉,改成把鎖加在鏈表的每個節點上。(也就是鎖粒度的細化)
那么,上述的情況下,A,B,C 3個線程就可以同時訪問各自的節點,特別是在多處理器的情況下,性能會有顯著提高。
最后還有一點需要提醒的是,鎖的粒度越細,系統開銷越大,程序也越復雜,所以對於爭用不是很頻繁的鎖,就沒有必要細化了。