深入解析Linux併發同步

2021-10-11 02:52:42 字數 3443 閱讀 3063

併發

是指在某一時間段內能夠處理多個任務的能力,而 並行 是指同一時間能夠處理多個任務的能力。併發和並行看起來很像,但實際上是有區別的,如下圖:

在這裡插入描述

上圖的意思是,有兩條在排隊買咖啡的佇列,併發只有一架咖啡機在處理,而並行就有兩架的咖啡機在處理。咖啡機的數量越多,並行能力就越強。

可以把上面的兩條佇列看成兩個程序,併發就是指只有單個cpu在處理,而並行就有兩個cpu在處理。為了讓兩個程序在單核cpu中也能得到執行,一般的做法就是讓每個程序交替執行一段時間,比如讓每個程序固定執行 100毫秒,執行時間使用完後切換到其他程序執行。而並行就沒有這種問題,因為有兩個cpu,所以兩個程序可以同時執行。如下圖:

在這裡插入描述

原子操作

上面介紹過,併發有可能會打斷當前執行的程序,然後替切換成其他程序執行。如果有兩個程序同時對乙個共享變數 count 進行加一操作,由於c語言的 count++ 操作會被翻譯成如下指令:

mov eax, [count]

inc eax

mov [count], eax12

3那麼在併發的情況下,有可能出現如下問題:

在這裡插入描述

假設count變數初始值為0:

程序1執行完 mov eax, [count] 後,暫存器eax內儲存了count的值0。

程序2被排程執行。程序2執行 count++ 的所有指令,將累加後的count值1寫回到記憶體。

程序1再次被排程執行,計算count的累加值仍為1,寫回到記憶體。

雖然程序1和程序2執行了兩次 count++ 操作,但是count最後的值為1,而不是2。

要解決這個問題就需要使用 原子操作,原子操作是指不能被打斷的操作,在單核cpu中,一條指令就是原子操作。比如上面的問題可以把 count++ 語句翻譯成指令 inc [count] 即可。linux也提供了這樣的原子操作,如對整數加一操作的 atomic_inc():

staticinlinevoid atomic_inc(atomic_t *v)12

3456

7在多核cpu中,一條指令也不一定是原子操作,比如 inc [count] 指令在多核cpu中需要進行如下過程:

從記憶體將count的資料讀取到cpu。

累加讀取的值。

將修改的值寫回count記憶體。

intel x86 cpu 提供了 lock 字首來鎖住匯流排,可以讓指令保證不被其他cpu中斷,如下:

lock

inc [count]12

鎖原子操作 能夠保證操作不被其他程序干擾,但有時候乙個複雜的操作需要由多條指令來實現,那麼就不能使用原子操作了,這時候可以使用 鎖 來實現。

電腦科學中的 鎖 與日常生活的 鎖 有點類似,舉個例子:比如要上公廁,首先找到乙個沒有人的廁所,然後把廁所門鎖上。其他人要使用的話,必須等待當前這人使用完畢,並且把門鎖開啟才能使用。在計算機中,要對某個公共資源進行操作時,必須對公共資源進行上鎖,然後才能使用。如果不上鎖,那麼就可能導致資料混亂的情況。

在linux核心中,比較常用的鎖有:自旋鎖、訊號量、讀寫鎖 等,下面介紹一下自旋鎖和訊號量的實現。

使用 自旋鎖 時,必須先對自旋鎖進行初始化(設定為1),上鎖過程如下:

對自旋鎖 lock 進行減一操作,判斷結果是否等於0,如果是表示上鎖成功並返回。

如果不等於0,表示其他程序已經上鎖,此時必須不斷比較自旋鎖 lock 的值是否等於1(表示已經解鎖)。

如果自旋鎖 lock 等於1,跳轉到第一步繼續進行上鎖操作。

由於linux的自旋鎖使用彙編實現,所以比較苦澀難懂,這裡使用c語言來模擬一下:

void spin_lock(amtoic_t *lock)

while (true) 

}

}12

3456

78910

1112

1314

上面**將 result = --(*lock); 當成原子操作,解鎖過程只需要把 lock 設定為1即可。由於自旋鎖會不斷嘗試上鎖操作,並不會對程序進行排程,所以在單核cpu中可能會導致 100% 的cpu佔用率。另外,自旋鎖只適合粒度比較小的操作,如果操作粒度比較大,就需要使用訊號量這種可排程程序的鎖。

訊號量與 自旋鎖 不一樣,當當前程序對 訊號量 進行上鎖時,如果其他程序已經對其進行上鎖,那麼當前程序會進入睡眠狀態,等待其他程序對訊號量進行解鎖。過程如下圖:

在這裡插入描述

在linux核心中,訊號量使用 struct semaphore 表示,定義如下:

struct semaphore ;12

345各個欄位的作用如下:

lock:自旋鎖,用於對多核cpu平台進行同步。

count:訊號量的計數器,上鎖時對其進行減一操作(count–),如果得到的結果為大於等於0,表示成功上鎖,如果小於0表示已經被其他程序上鎖。

wait_list:正在等待訊號量解鎖的程序佇列。

訊號量 上鎖通過 down() 函式實現,**如下:

void down(struct semaphore *sem)12

3456

78910

11上面**可以看出,down() 函式首先對訊號量進行自旋鎖操作(為了避免多核cpu競爭),然後比較計數器是否大於0,如果是對計數器進行減一操作,並且返回,否則呼叫 __down() 函式進行下一步操作。__down() 函式實現如下:

static noinline void __sched __down(struct semaphore *sem)

static inline int __down_common(struct semaphore *sem,

long state, long timeout)

...}12

3456

78910

1112

1314

1516

1718

1920

2122

2324

2526

27__down() 函式最終呼叫 __down_common() 函式,而 __down_common() 函式的操作過程如下:

把當前程序新增到訊號量的等待佇列中。

切換到其他程序執行,直到被其他程序喚醒。

如果當前程序獲得訊號量鎖(由解鎖程序傳遞),那麼函式返回。

接下來看看解鎖過程,解鎖過程主要通過 up() 函式實現,**如下:

void up(struct semaphore *sem)12

3456

78910

1112

1314

1516

1718

1920

2122

解鎖過程如下:

判斷當前訊號量是否有等待的程序,如果沒有等待的程序, 直接對計數器加一操作

如果有等待的程序,那麼獲取到等待佇列的第乙個程序。

把程序從等待佇列中刪除。

告訴程序已經獲得訊號量鎖。

喚醒程序。

深入解析Linux併發同步

是指在某一時間段內能夠處理多個任務的能力,而 並行 是指同一時間能夠處理多個任務的能力。併發和並行看起來很像,但實際上是有區別的,如下圖 上圖的意思是,有兩條在排隊買咖啡的佇列,併發只有一架咖啡機在處理,而並行就有兩架的咖啡機在處理。咖啡機的數量越多,並行能力就越強。epoll的具體實現與epoll...

LINUX c 併發同步

5.1 核心同步與死鎖問題 併發 兩個程序可以真正的在臨界區中同時執行。原因 1 中斷 2 軟中斷和tasklet 3 核心搶占 4 睡眠及與使用者空間的同步 5 對稱多處理 資料加鎖 1 如果有其他執行執行緒可以訪問這些資料 2 如果任何其他什麼東西都能看見它 3 幾乎訪問所有的核心全域性變數和共...

深入同步語法

1.深入synchronized關鍵字 class servicecatch exception e system.outprintln fun1 public void fun2 class mythread1 implements runnable public void run class m...