1. 程式人生 > >執行緒同步(2):原子操作原理

執行緒同步(2):原子操作原理

1.原子操作的原理是通過鎖記憶體匯流排的方式實現原子操作的

原子操作

假定執行在兩個CPU上的兩個核心控制路徑試圖執行非原子操作同時-修改-同一儲存器單元。首先,兩個CPU都試圖讀同一單元,但是儲存器仲裁器插手,只允許其中的一個訪問而讓另一個延遲。然而,當第一個讀操作已經完成後,延遲的CPU從那個儲存器單元正好讀到同一個()值。然後,兩個CPU都試圖向那個儲存器單元寫一新值,匯流排儲存器訪問再一次被儲存器仲裁器序列化,最終,兩個寫操作都成功。但是,全域性的結果是不對的,因為兩個CPU寫入同一()值。因此,兩個交錯的"-修改-"操作成了一個單獨的操作。

避免由於-修改-

指令引起競爭條件的最容易的辦法,就是確保這樣的操作在晶片級是原子的。任何一個這樣的操作都必須一個單個指令執行,中間不能中斷,且避免其他CPU訪問同一儲存單元。

在你編寫C程式碼程式時,並不能保證編譯器會為a++這樣的操作使用一個原子指令。原子操作需要硬體的支援,因此是架構相關的,它們都使用匯編語言實現,因為C語言並不能實現這樣的操作。因此,Linux核心提供了一個專門的atomic_t型別和一些專門的函式和巨集。原子操作主要用於實現資源計數,很多引用計數(refcnt)就是通過原子操作實現的。

原子型別定義如下:

1. typedef struct {  

2.     volatile int

 counter;  

3. } atomic_t;  

volatile修飾字段告訴gcc不要對該型別的資料做優化處理,對它的訪問都是對記憶體的訪問,而不是對暫存器的訪問。

Linux中的原子操作

atomic_read(v)      返回*v

atomic_set(v,i)     *v置成i

atomic_add(i,v)     *v增加i

atomic_sub(i,v)     *v減去i

atomic_sub_and_test(i,v) *v減去i,如果結果0,則返回1;否則,返回0

atomic_inc(v)       *v1

atomic_dec(v)       *v1

atomic_dec_and_test(v)   *v減去1,如果結果0,則返回1;否則,返回0

atomic_inc_and_test(v)   *v加上1,如果結果0,則返回1;否則,返回0

atomic_add_negative(i,v) i加到*v,如果結果為負,則返回1;否則,返回0

atomic_add_return(i,v)   *vi,返回*v的新值

atomic_sub_return(i,v)   *vi,返回*v的新值

1. #ifdef CONFIG_SMP  

2. #define LOCK "lock ; "  

3. #else  

4. #define LOCK ""  

5. #endif  

6. /** 

7.  * atomic_add - add integer to atomic variable 

8.  * @i: integer value to add 

9.  * @v: pointer of type atomic_t 

10.  *  

11.  * Atomically adds @i to @v. 

12.  */  

13. static __inline__ void atomic_add(int i, atomic_t *v)  

14. {  

15.     __asm__ __volatile__(  

16.         LOCK "addl %1,%0"  

17.         :"=m" (v->counter)  

18.         :"ir" (i), "m" (v->counter));  

19. }  

在多核處理器系統中,每條指令都有一個lock位元組的字首。當控制單元檢測到這個字首時,就鎖定記憶體匯流排,直到這條指令執行完為止。因此,當加鎖的指令執行時,其他處理器不能訪問這個記憶體單元。

Linux中的原子位處理函式

test_bit(nr,addr)            返回*addr的第nr位的值

set_bit(nr,addr)             設定*addr的第nr位的值

clear_bit(nr,addr)           *addr的第nr

change_bit(nr,addr)          轉換*addr的第nr

test_and_set_bit(nr,addr)    設定*addr的第nr位,並返回它的原值

test_and_clear_bit(nr,addr)  *addr的第nr位,並返回它的原值

test_and_change_bit(nr,addr) 轉換*addr的第nr位,並返回它的原值

atomic_clear_mask(mask,addr) 清除mask指定的*addr的所有位

atomic_set_mask(mask,addr)   設定mask指定的*addr的所有位

需要使用atomic_add_return函式,發現編譯時找不到該函式的入口,閱讀iatomic.h後發現在I386X86_64平臺下,不支援帶有return字尾函式,例如atomic_sub_returnatomic_inc_return等,但支援test字尾函式,如atomic_sub_and_test等。

在其它平臺,如powerpcmipsarm下支援全系列的原子操作。真不知設計這個標頭檔案的人是怎麼想的。

/usr/include/alsa/iatomic.h,也能實現原子操作,使用的時候#include<alsa/iatomic.h>就可以了,原有的atomic系列函式這裡都有,包括atomic_readatomic_setatomic_incatomic_addatomic_sub,但是該函式在RedHat(RHEL)6.0以上版本也被取消了。

只支援32位資料的原子操作。

-------------------------------正文---------------------------------------

     Linux2.6.18之後,刪除了<asm/atomic.h><asm/bitops.h>GCC提供了內建的原子操作函式,更適合使用者態的程式使用。現在atomic.h在核心標頭檔案中,不在gcc預設搜尋路徑下,即使像下面這樣強行指定路徑,還是會出現編譯錯誤。

[cpp]view plaincopyprint?

#include</usr/src/linux-headers-2.6.27-7/include/asm-x86/atomic.h>  

gcc4.1.0提供了__sync_*系列的built-in函式,用於提供加減和邏輯運算的原子操作,我是傳送門

可以對1,2,48位元組長度的數值型別或指標進行原子操作,其宣告如下

[cpp]view plaincopyprint?

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, ...)  

          { tmp = *ptr; *ptr op= value; return tmp; }  

          { tmp = *ptr; *ptr = ~tmp & value; return tmp; }   // nand  

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, ...)  

          { *ptr op= value; return *ptr; }  

          { *ptr = ~*ptr & value; return *ptr; }   // nand  

這兩組函式的區別在於第一組返回更新前的值,第二組返回更新後的值,下面的示例引自這裡

[cpp]view plaincopyprint?

#include <stdio.h>  

#include <pthread.h>  

#include <stdlib.h>  

static int count = 0;  

void *test_func(void *arg)  

{  

        int i=0;  

        for(i=0;i<20000;++i){  

                __sync_fetch_and_add(&count,1);  

        }  

        return NULL;  

}  

int main(int argc, const char *argv[])  

{  

        pthread_t id[20];  

        int i = 0;  

        for(i=0;i<20;++i){  

                pthread_create(&id[i],NULL,test_func,NULL);  

        }  

        for(i=0;i<20;++i){  

                pthread_join(id[i],NULL);  

        }  

        printf("%d/n",count);  

        return 0;  

}  

對於使用atomic.h的老程式碼,可以通過巨集定義的方式,移植到高核心版本的linux系統上,例如

[cpp]view plaincopyprint?

#define atomic_inc(x) __sync_add_and_fetch((x),1)  

#define atomic_dec(x) __sync_sub_and_fetch((x),1)  

#define atomic_add(x,y) __sync_add_and_fetch((x),(y))  

#define atomic_sub(x,y) __sync_sub_and_fetch((x),(y))  

  當然我們知道,count++這種操作不是原子的。一個自加操作,本質是分成三步的:
     1 從快取取到暫存器
     2 在暫存器加1
     3 存入快取。
    由於時序的因素,多個執行緒操作同一個全域性變數,會出現問題。這也是併發程式設計的難點。在目前多核條件下,這種困境會越來越彰顯出來。
    最簡單的處理辦法就是加鎖保護,這也是我最初的解決方案。看下面的程式碼:
      pthread_mutex_t count_lock = PTHREAD_MUTEX_INITIALIZER;

      pthread_mutex_lock(&count_lock);
      global_int++;
      pthread_mutex_unlock(&count_lock);
    後來在網上查詢資料,找到了__sync_fetch_and_add系列的命令,發現這個系列命令講的最好的一篇文章,英文好的同學可以直接去看原文。 Multithreaded simple data type access and atomic variables

     __sync_fetch_and_add系列一共有十二個函式,有加/減/與/或/異或/等函式的原子性操作函 數,__sync_fetch_and_add,顧名思義,現fetch,然後自加,返回的是自加以前的值。以count = 4為例,呼叫__sync_fetch_and_add(&count,1),之後,返回值是4,然後,count變成了5.
    有__sync_fetch_and_add,自然也就有__sync_add_and_fetch,呵呵這個的意思就很清楚了,先自加,在返回。他們哥倆的關係與i++和++i的關係是一樣的。被譚浩強他老人家收過保護費的都會清楚了。
    有了這個寶貝函式,我們就有新的解決辦法了。對於多執行緒對全域性變數進行自加,我們就再也不用理執行緒鎖了。下面這行程式碼,和上面被pthread_mutex保護的那行程式碼作用是一樣的,而且也是執行緒安全的。

__sync_fetch_and_add( &global_int, 1 );
    下面是這群函式的全家福,大家看名字就知道是這些函式是幹啥的了。

在用gcc編譯的時候要加上選項 -march=i686
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);

// sam:很納悶為什麼後邊要寫省略號,是不是還有不需要我們關心的引數?用的時候不需要傳引數?下面這兩個函式正是哥想要的啦,可以輕鬆實現互斥鎖的功能。

bool __sync_bool_compare_and_swap (type*ptr, type oldval, type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval,  type newval, ...)這兩個函式提供原子的比較和交換,如果*ptr == oldval,就將newval寫入*ptr,
第一個函式在相等並寫入的情況下返回true.
第二個函式在返回操作之前的值。
__sync_synchronize (...)

理解上面這個東西,參照:http://blog.sunchangming.com/post/47188394133
還有兩個函式:
type __sync_lock_test_and_set (type *ptr, type value, ...)*ptr設為value並返回*ptr操作之前的值。
void __sync_lock_release (type *ptr, ...)*ptr0


    需要提及的是,這個type不能夠瞎搞。下面看下__sync_fetch_and_add反彙編出來的指令,
804889d: f0 83 05 50 a0 04 08 lock addl $0x1,0x804a050
    我們看到了,addl前面有個lock,這行彙編指令碼前面是f0開頭,f0叫做指令字首,Richard Blum
老爺子將指令字首分成了四類,有興趣的同學可以看下。其實我也沒看懂,intel的指令集太厚了,沒空看。總之老爺子解釋了,lock字首的意思是對記憶體區域的排他性訪問。
? Lock and repeat prefixes
? Segment override and branch hint prefixes
? Operand size override prefix
? Address size override prefix

     前文提到,lock是鎖FSB,前端序列匯流排,front serial bus,這個FSB是處理器和RAM之間的匯流排,鎖住了它,就能阻止其他處理器或者core從RAM獲取資料。當然這種操作是比較費的,只能操作小的記憶體 可以這樣做,想想我們有memcpy ,如果操作一大片記憶體,鎖記憶體,那麼代價就太昂貴了。所以前文提到的_sync_fetch_add_add家族,type只能是int long  ,long long(及對應unsigned型別)。

     下面提供了函式,是改造的Alexander Sundler的原文,榮譽屬於他,我只是學習他的程式碼,稍微改動了一點點。比較了兩種方式的耗時情況。呵呵咱是菜鳥,不敢枉自剽竊大師作品。向大師致敬。
#define _GNU_SOURCE

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sched.h>
#include <linux/unistd.h>
#include <sys/syscall.h>
#include <errno.h>
#include<linux/types.h>
#include<time.h>

#define INC_TO 1000000 // one million...

__u64 rdtsc()
{
  __u32 lo,hi;

    __asm__ __volatile__
    (
     "rdtsc":"=a"(lo),"=d"(hi)
    );

    return (__u64)hi<<32|lo;
}

int global_int = 0;
pthread_mutex_t count_lock = PTHREAD_MUTEX_INITIALIZER;


pid_t gettid( void )
{
    return syscall( __NR_gettid );
}

void *thread_routine( void *arg )
{
    int i;
    int proc_num = (int)(long)arg;
    __u64 begin, end;
    struct timeval tv_begin,tv_end;
    __u64 timeinterval;
    cpu_set_t set;

    CPU_ZERO( &set );
    CPU_SET( proc_num, &set );

    if (sched_setaffinity( gettid(), sizeof( cpu_set_t ), &set ))
    {
        perror( "sched_setaffinity" );
        return NULL;
    }

    begin = rdtsc();
    gettimeofday(&tv_begin,NULL);
    for (i = 0; i < INC_TO; i++)
    {
//     global_int++;
        __sync_fetch_and_add( &global_int, 1 );
    }
    gettimeofday(&tv_end,NULL);
    end = rdtsc();
    timeinterval =(tv_end.tv_sec - tv_begin.tv_sec)*1000000                    +(tv_end.tv_usec - tv_begin.tv_usec);
    fprintf(stderr,"proc_num :%d,__sync_fetch_and_add cost             %llu CPU cycle,cost %llu us\n",                             proc_num,end-begin,timeinterval);

    return NULL;
}


void *thread_routine2( void *arg )
{
    int i;
    int proc_num = (int)(long)arg;
    __u64 begin, end;

    struct timeval tv_begin,tv_end;
    __u64 timeinterval;
    cpu_set_t set;

    CPU_ZERO( &set );
    CPU_SET( proc_num, &set );

    if (sched_setaffinity( gettid(), sizeof( cpu_set_t ), &set ))
    {
        perror( "sched_setaffinity" );
        return NULL;
    }


    begin = rdtsc();
    gettimeofday(&tv_begin,NULL);

    for(i = 0;i<INC_TO;i++)
    {
        pthread_mutex_lock(&count_lock);
        global_int++;
        pthread_mutex_unlock(&count_lock);
    }

    gettimeofday(&tv_end,NULL);
    end = rdtsc();


    timeinterval =(tv_end.tv_sec - tv_begin.tv_sec)*1000000                   +(tv_end.tv_usec - tv_begin.tv_usec);
    fprintf(stderr,"proc_num :%d,pthread lock cost %llu CPU                    cycle,cost %llu us\n",proc_num,end-begin                    ,timeinterval);

    return NULL;
}
int main()
{
    int procs = 0;
    int i;
    pthread_t *thrs;

    // Getting number of CPUs
    procs = (int)sysconf( _SC_NPROCESSORS_ONLN );
    if (procs < 0)
    {
        perror( "sysconf" );
        return -1;
    }

    thrs = malloc( sizeof( pthread_t ) * procs );
    if (thrs == NULL)
    {
        perror( "malloc" );
        return -1;
    }

    printf( "Starting %d threads...\n", procs );

    for (i = 0; i < procs; i++)
    {
        if (pthread_create( &thrs[i], NULL, thread_routine,
            (void *)(long)i ))
        {
            perror( "pthread_create" );
            procs = i;
            break;
        }
    }

    for (i = 0; i < procs; i++)
        pthread_join( thrs[i], NULL );

    free( thrs );

    printf( "After doing all the math, global_int value is:              %d\n", global_int );
    printf( "Expected value is: %d\n", INC_TO * procs );

    return 0;
}
     通過我的測試發現:

Starting 4 threads...
proc_num :2,no locker cost 27049544 CPU cycle,cost 12712 us
proc_num :0,no locker cost 27506750 CPU cycle,cost 12120 us
proc_num :1,no locker cost 28499000 CPU cycle,cost 13365 us
proc_num :3,no locker cost 27193093 CPU cycle,cost 12780 us
After doing all the math, global_int value is: 1169911
Expected value is: 4000000
Starting 4 threads...
proc_num :2,__sync_fetch_and_add cost 156602056 CPU cycle,cost 73603 us
proc_num :1,__sync_fetch_and_add cost 158414764 CPU cycle,cost 74456 us
proc_num :3,__sync_fetch_and_add cost 159065888 CPU cycle,cost 74763 us
proc_num :0,__sync_fetch_and_add cost 162621399 CPU cycle,cost 76426 us
After doing all the math, global_int value is: 4000000
Expected value is: 4000000

Starting 4 threads...
proc_num :1,pthread lock cost 992586450 CPU cycle,cost 466518 us
proc_num :3,pthread lock cost 1008482114 CPU cycle,cost 473998 us
proc_num :0,pthread lock cost 1018798886 CPU cycle,cost 478840 us
proc_num :2,pthread lock cost 1019083986 CPU cycle,cost 478980 us
After doing all the math, global_int value is: 4000000
Expected value is: 4000000
1 不加鎖的情況下,不能返回正確的結果
  測試程式結果顯示,正確結果為400萬,實際為1169911.

2 執行緒鎖和原子性自加都能返回正確的結果。

3 效能上__sync_fetch_and_add,完爆執行緒鎖。
  從測試結果上看, __sync_fetch_and_add,速度是執行緒鎖的6~7倍