您現在的位置是:首頁 > 棋牌

C語言陷阱與技巧19節,給一個變數加 0 也有講究?怎樣自定義一個自己的鎖?

  • 由 IT劉小虎 發表于 棋牌
  • 2021-05-26
簡介根據改宏的名字,應該能夠知道它是“原子的”讀取,而一個被讀取的資料再做“左值”顯然是不合適的,如果沒有後面的 “+0”,下面這樣誤寫的C語言程式碼,編譯器是不會報錯的:if (atomic_read(v) = 32) {

c語言餘數為0怎麼表示

在閱讀 Linux 核心原始碼時,發現了兩個宏,相關的C語言程式碼如下,請看:

#define atomic_read(v) ((v)->counter + 0)

#define atomic64_read(v) ((v)->counter + 0)

這兩個宏接收一個結構體引數,該結構體的C語言定義如下:

typedefstruct { volatileint counter; } atomic_t;

typedefstruct { volatilelong counter; } atomic64_t;

這兩個宏可以提供“原子操作”級的讀資料操作。一開始看到這個宏的時候,我搞不懂為何要在最後“+0”,不過仔細想想,這麼做至少有兩個好處。

宏定義後 “+0”的技巧

首先,在 atomic_read() 宏定義後“+0”可以避免 atomic_read() 宏被當作“左值”。根據改宏的名字,應該能夠知道它是“原子的”讀取,而一個被讀取的資料再做“左值”顯然是不合適的,如果沒有後面的 “+0”,下面這樣誤寫的C語言程式碼,編譯器是不會報錯的:

if (atomic_read(v) = 32) {

。。。

}

當然,也可以使用下面這樣的宏定義避免 atomic_read() 宏被當作左值:

#define atomic_read(v) (+(v)->counter)

即加上一個“正號”,不過這麼做顯然沒有在後面“+0”好,相信讀者應該明白,這裡就不贅述了。

C語言陷阱與技巧19節,給一個變數加 0 也有講究?怎樣自定義一個自己的鎖?

按照C語言標準,一個宏只要名字一樣,引數型別一樣,邏輯一樣,出現重複的宏定義時完全沒有問題的,不過出現重複程式碼對維護來說是一件很不好的事。在宏後面“+0”的另外一個好處就是可以儘可能的避免重複的宏定義。請看:

//atomic。h

#define atomic_read(v) ((v)->counter + 0)

#define atomic64_read(v) ((v)->counter)

//some source file that includes atomic。h

#define atomic_read(v) ((v)->counter) //redefinition error

#define atomic64_read(v) ((v)->counter) //no redefinition error

C語言陷阱與技巧19節,給一個變數加 0 也有講究?怎樣自定義一個自己的鎖?

C語言程式開發中的原子操作

我們再來說說C語言程式開發中的“原子操作”。相信不少朋友都聽說過“鎖”的概念,它主要用於避免一些共享資源被多個執行緒併發訪問時,出現數據錯誤的情況。而“原子操作”是鎖的基石,或者換句話說,“鎖”是依靠原子操作實現的。

眾所周知,“原子”是組成萬物的微小顆粒,一般認為原子已經足夠小,無法再被分割。與之對應,C語言中的“原子操作”則是不能再被分割的指令。那麼,原子操作的意義是什麼呢?假設在某個C語言程式中定義了一個全域性變數 i,如果有兩個執行緒同時訪問 i,並執行“加一”操作,如果 i 的初值為 0,我們當然希望這一過程是這樣的:

C語言陷阱與技巧19節,給一個變數加 0 也有講究?怎樣自定義一個自己的鎖?

但是,如果沒有對 i 做任何保護,實際上非常有可能是下面這樣的執行流程:

C語言陷阱與技巧19節,給一個變數加 0 也有講究?怎樣自定義一個自己的鎖?

因為

訪問

i和對其

加一

是獨立的兩個過程,執行緒1和執行緒2完全可能在 i 的值增加之前讀取到了它的初值,然後各自加一,這就會導致不期望的結果出現:兩個執行緒執行完畢後,全域性變數 i 的值本來應該是 2 的,結果卻為 1 了。

不過,如果

訪問

加一

這兩個操作是原子操作,上面那種競爭情況就不會出現了,整個過程只有可能是下面這兩種情況之一:

C語言陷阱與技巧19節,給一個變數加 0 也有講究?怎樣自定義一個自己的鎖?

最後必定會得到預期結果(i==2),因為

訪問

加一

是一個原子操作,這個過程不可能被分割,也就不會出現不預期的結果了。

原子操作的“陷阱”與“小技巧”

可能初學者會認為C語言程式中,如果程式碼只有一行,那必定是原子操作。這其實是一個較為致命的“陷阱”,大多數機器只能保證操作一個字是原子的,還有一部分機器則只能保證操作一個位元組是原子的。

舉個最簡單的例子,請看下面這段C語言程式碼:

struct s{

long a;

double b;

char c[1024];

};

struct s s1, s2;

s1 = s2;

s 是一個相當大的結構體,所以 s1=s2; 雖然只有一行,它仍然不是原子操作。事實上,我們可以得到這段C語言程式的彙編程式碼:

C語言陷阱與技巧19節,給一個變數加 0 也有講究?怎樣自定義一個自己的鎖?

容易看出,雖然 s1=s2; 只是一行C語言程式碼,但機器卻需要若干條指令才能完成,這就非常有可能被其他執行緒打斷。例如 s1=s2; 這條語句正在賦值,還沒有完成時,其他執行緒讀取了 s1,這顯然會導致不期望的結果出現。

避免出現上述“不期望”的結果出現的方法就是對 s1 和 s2 進行保護。常用的方法是使用鎖,在賦值之前加鎖,賦值完成後再解鎖。

lock();s1 = s2;unlock();

互斥鎖是使用最廣泛的鎖之一,但是互斥鎖在加鎖過程中可能會睡眠,這時作業系統可能會排程其他執行緒執行,這對於需要較長時間加鎖的情況當然是好事,但是我們僅做了賦值操作,是不希望有這樣的時間開銷的,針對這種情況,一個小技巧是使用位操作,自定義一個輕量級的鎖:

status char status = 0;

#define BIT_LOCK 0x01

#define BIT_UNLOCK() (status &= ~BIT_LOCK)

#define BIT_LOCK()\

do{\

while( status & BIT_LOCK);\

status |= BIT_LOCK;\

}while(0)

BIT_LOCK();

s1 = s2;

BIT_UNLOCK();

C語言陷阱與技巧19節,給一個變數加 0 也有講究?怎樣自定義一個自己的鎖?

一個位要麼是 0,要麼是 1,它的變化必定是“原子”的,因此完全可以用來自定義一個輕量級的鎖。不過應該注意,BIT_LOCK() 只是一個輕量級的鎖,在鎖住資源的時候,它不會睡眠,而是讓 CPU 保持空轉等待,這期間CPU什麼工作也不做。所以在 BIT_LOCK() 加鎖期間,應只做一些能夠快速完成的工作。

請讀者思考一下,為什麼在處理一些能夠快速完成的工作時,使用 BIT_LOCK() 比使用互斥鎖的效率反而更高呢?(可閱讀我的 《Linux 學習》系列文章)

小結

本節我們透過Linux 核心中的一個宏定義知道了有時候“+0”這樣看似無用的操作,也是能夠提供非常不錯的實用技巧的。另外,本節也討論了原子操作,以及如何利用這一原理實現自己的鎖。

Top