1. 程式人生 > >多執行緒基礎之四:Linux提供的原子鎖型別atomic_t

多執行緒基礎之四:Linux提供的原子鎖型別atomic_t

在x86體系下,任何處理器平臺下都會有一些原子性操作,在單處理器情況下,單步指令的原子性容易實現。但是在SMP多處理器情況下,只有那些單字的讀(將變數讀進暫存器)或寫(從暫存器寫入到變數地址)才是原子性的。故而在SMP下,要保證特定指令集合的原子性即不被中斷,x86提供lock字首用來在執行被lock修飾的指令期間鎖住匯流排,從而實現了“禁止中斷”的效果。事實上,Linux作業系統便根據這種針對特殊資料物件在操作期間需要提供原子特性的情況專門提供atomic_t為代表的“原子鎖”。


0. Linux下atomic_t原子鎖和操作函式API

原子操作需要硬體支援,因此是和計算機的具體架構相關的,Linux專門定義了一種原子操作粒度的型別atomic_t (類似的還有atomic6_t),併為該原子型別引數的操作提供相對應的API。故而在Linux下程式設計使用原子鎖,只需要呼叫該型別宣告和相應的操作函式API即可。

原子型別引數定義的典型使用場景便是多程序中共享資源的計數加減,如訊號量semaphores中的資源總數便是經典的使用場景。所以atomic_t原子鎖支援的便是聲明瞭一個具有原子操作特性的整數。

下面開始來介紹一下相關的內容。

typedef struct{
    volatile int counter; //volatile修飾符高速gcc不要對該型別資料進行優化處理,即對它的訪問都是對 
          //記憶體的訪問,而不是對暫存器的訪問。即要讀,必須重新找個暫存器載入該引數,而不是直接利用該引數
          //此刻在其他高速暫存器中的備份。
} atomic_t;

Linux為原子型別引數提供了一些系列的操作函式API,收集列舉如下:

這些操作函式的實現均涉及到在C中使用內嵌組合語言,以其中atomic_add(int i, atomic_t)的具體實現為例

static inline void atomic_add(int i, atomic_t *v)
{
    asm volatile(LOCK_PREFIX "addl %1,%0"
             : "+m" (v->counter)
             : "ir" (i));
}

1. Linux下atomic.h的替代者__syn_*系列函式

在Linux2.6.18之後,系統便刪除了<asm/atomic.h><asm/bitops.h><alsa/iatomic.h>,在Linux作業系統下GCC提供了內建的原子操作函式__sync_*,更方便程式設計師呼叫。

現在atomic.h在Linux的核心標頭檔案中,即便能搜尋到,但依舊不在gcc預設搜尋路徑下(/usr/include,/usr/local/include,/usr/lib/gcc-lib/i386-linux/x.xx.x/include

),即使像下面這樣強行指定路徑,還是會出現編譯錯誤。

#include</usr/src/linux-headers-4.4.0-98/include/asm-generic/atomic.h> 
或在編譯時提供編譯路徑
 -I /usr/src/linux-headers-4.4.0-98/include/asm-generic依舊會出現問題

gcc從4.1.2提供了__sync_*系列的built-in函式,用於提供加減和邏輯運算的原子操作。可以對1,2,4或8位元組長度的數值型別或指標進行原子操作,其宣告如下

type __sync_fetch_and_add (type *ptr, type value, ...)
type __sync_fetch_and_sub (type *ptr, type value, ...)
type __sync_fetch_and_or (type *ptr, type value, ...)
type __sync_fetch_and_and (type *ptr, type value, ...)
type __sync_fetch_and_xor (type *ptr, type value, ...)
type __sync_fetch_and_nand (type *ptr, type value, ...)


type __sync_add_and_fetch (type *ptr, type value, ...)
type __sync_sub_and_fetch (type *ptr, type value, ...)
type __sync_or_and_fetch (type *ptr, type value, ...)
type __sync_and_and_fetch (type *ptr, type value, ...)
type __sync_xor_and_fetch (type *ptr, type value, ...)
type __sync_nand_and_fetch (type *ptr, type value, ...)

故而現在如果要使得atomic.h的舊版本程式碼可以執行在當下較新的Linux版本下,需要在相應的程式碼檔案前面設定巨集替換舊版本的atomic_*系列函式

    #define atomic_inc(x) __sync_fetch_and_add((x),1)  
    #define atomic_dec(x) __sync_fetch_and_sub((x),1)  
    #define atomic_add(x,y) __sync_fetch_and_add((x),(y))  
    #define atomic_sub(x,y) __sync_fetch_and_sub((x),(y))  

2. atomic_t原子鎖使用案例

normal.cpp
#include <unistd.h>
#include <pthread.h>
#include <iostream>

using namespace std;

//設計test class帶有兩個正常的引數a,b;讓它們實現正常的自增,看看是否出現因為非原子性操作導致的變數不同步

class test{

    private:
        int a;
        int b;

    public:
        test():a(0),b(0.0){}
        void inc(){//對變數自增,正常來說因為a,b並沒有被宣告為原子鎖引數,故而這種自增操作是非執行緒安全的
            a++;
            b++;
        }
        void get() const{
            cout<<"a="<<a<<" b="<<b<<endl;
        }

};

static int step=0;

void* worker(void* arg){
    sleep(100-step); //手動延遲,因為建立執行緒還蠻費時間的,所以要人為創造多執行緒併發操作目標物件的情況
    step++;
    //每個執行緒睡眠一定時間以期達到這些執行緒能同時對變數進行操作,當然這裡本身就不是執行緒安全的,因為結果得到:自增不是執行緒安全的
    test* local_test =(test*)arg; //將目標物件淺拷貝
    local_test->inc();
}

int main(){
    pthread_t pthd[100]; //宣告一個執行緒指標陣列
    test* temp=new test;
    for(int i=0;i<100;i++){
        pthread_create(&pthd[i],NULL,worker,temp);//開啟100個執行緒對變數自增
    }
    for(int i=0;i<100;i++){
        pthread_join(pthd[i],NULL); //等待所有子執行緒完成操作,否則main執行緒將提前執行後續操作
    }
    temp->get();//獲取結果
    return 0;
}

在Linux下編譯該檔案,如果運氣夠好,是可以發現該程式是可能出現執行緒不安全的情況

$g++ normal.cpp -o normal -lpthread
$./normal
//可能輸出a=99,b=99,也可能輸出a=98,b=98

atomic_t.cpp

#include <unistd.h>
#include <pthread.h>
#include <iostream>
#include <stdlib.h>

using namespace std;

#define atomic_inc(x)  __sync_fetch_and_add(x, 1)
//用GCC內嵌的__sync_*系列函式來替代原先的atomic_inc(atomic_t* v)函式

class test{

    private:
        int a;
        int b;

    public:
        test():a(0),b(0){}
        void inc(){//對變數自增,正常來說因為a,b並沒有被宣告為原子鎖引數,故而這種自增操作是非執行緒安全的
            //__sync_fetch_and_add(&a,1);
            //__sync_fetch_and_add(&b,1);
        atomic_inc(&a);
        atomic_inc(&b);
        }
        void get() const{
            cout<<"a="<<a<<" b="<<b<<endl;
        }

};

static int step=0;

void* worker(void* arg){
    sleep(100-step); //手動延遲,因為建立執行緒還蠻費時間的,所以要人為創造多執行緒併發操作目標物件的情況
    step++;
    //每個執行緒睡眠一定時間以期達到這些執行緒能同時對變數進行操作,當然這裡本身就不是執行緒安全的,因為結果得到:自增不是執行緒安全的
    test* local_test =(test*)arg; //將目標物件淺拷貝
    local_test->inc();
}

int main(){
    pthread_t pthd[100]; //宣告一個執行緒指標陣列
    test* temp=new test;
    for(int i=0;i<100;i++){
        pthread_create(&pthd[i],NULL,worker,temp);//開啟100個執行緒對變數自增
    }
    for(int i=0;i<100;i++){
        pthread_join(pthd[i],NULL); //等待所有子執行緒完成操作,否則main執行緒將提前執行後續操作
    }
    temp->get();//獲取結果
    return 0;
}
$g++ atomic_t.cpp -o atomic_t -lpthread
$./atomic_t
a=100 b=100//但可以明顯感到耗時比前面未加鎖的版本更多