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