1. 程式人生 > >【Linux】多執行緒無鎖程式設計--原子計數操作:__sync_fetch_and_add等12個操作

【Linux】多執行緒無鎖程式設計--原子計數操作:__sync_fetch_and_add等12個操作

 最近自己做了一些涉及多執行緒程式設計的專案,其中就涉及到多執行緒間計數操作、共享狀態或者統計相關時間次數,這些都需要在多執行緒之間共享變數和修改變數,如此就需要在多執行緒間對該變數進行互斥操作和訪問。

        通常遇到多執行緒互斥的問題,首先想到的就是加鎖lock,通過加互斥鎖來進行執行緒間互斥,但是最近有看一些開源的專案,看到有一些同步讀和操作的原子操作函式——__sync_fetch_and_add系列的命令,然後自己去網上查詢一番,找到一篇博文有介紹這系列函式,學習一番後記錄下來。

首先,C/C++程式中count++這種操作不是原子的,一個自加操作,本質上分為3步:

  1. 從快取取到暫存器
  2. 在暫存器內加1
  3. 再存入快取
但是由於時序的因素,多執行緒操作同一個全域性變數,就會出現很多問題。這就是多執行緒併發程式設計的難點,尤其隨著計算機硬體技術的快速發展,多CPU多核技術更彰顯出這種困難。

通常,最簡單的方法就是加鎖保護,互斥鎖(mutex),這也是我使用最多的解決方案。大致程式碼如下:
pthread_mutex_t lock;
pthread_mutex_init(&lock,...);

pthread_mutex_lock(&lock);
count++;
pthread_mutex_unlock(&lock);

後來,在一些C/C++開源專案中,看到通過__sync_fetch_and_add一系列命令進行原子性操作,隨後就在網上查閱相關資料,發現有很多部落格都有介紹這系列函式。

__sync_fetch_and_add系列一共有12個函式,分別:加/減/與/或/異或等原子性操作函式,__sync_fetch_and_add,顧名思義,先fetch,返回自加前的值。舉例說明,count = 4,呼叫__sync_fetch_and_add(&count, 1)之後,返回值是4,但是count變成5。同樣,也有__sync_add_and_fetch,先自加,然後返回自加後的值。這樣對應的關係,與i++和++i的關係是一樣的。

gcc從4.1.2開始提供了__sync_*系列的build-in函式,用於提供加減和邏輯運算的原子操作,其宣告如下:

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

上述12個函式即為所有,通過函式名字就可以知道函式的作用。需要注意的是,這個type不能亂用(type只能是int, long, long long以及對應的unsigned型別),同時在用gcc編譯的時候要加上選項 -march=i686。
後面的可擴充套件引數(...)用來指出哪些變數需要memory barrier,因為目前gcc實現的是full barrier(類似Linux kernel中的mb(),表示這個操作之前的所有記憶體操作不會被重排到這個操作之後),所以可以忽略掉這個引數。

下面簡單介紹一下__sync_fetch_and_add反彙編出來的指令(實際上,這部分我還不是很懂,都是從其他部落格上摘錄的)
804889d:
f0 83 05 50 a0 04 08 lock addl $0x1,0x804a050

可以看到,addl前面有一個lock,這行彙編指令前面是f0開頭,f0叫做指令字首,Richard Blum。lock字首的意思是對記憶體區域的排他性訪問。

其實,lock是鎖FSB,前端序列匯流排,Front Serial Bus,這個FSB是處理器和RAM之間的匯流排,鎖住FSB,就能阻止其他處理器或者Core從RAM獲取資料。當然這種操作開銷相當大,只能操作小的記憶體可以這樣做,想想我們有memcpy,如果操作一大片記憶體,鎖記憶體,那麼代價太大了。所以前面介紹__sync_fetch_and_add等函式,type只能是int, long, long long以及對應的unsigned型別。

此外,還有兩個類似的原子操作,
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;
第二個函式在返回操作之前的值。 type __sync_lock_test_and_set(type *ptr, type value, ...) 將*ptr設為value並返回*ptr操作之前的值; void __sync_lock_release(type *ptr, ...) 將*ptr置為0 有了這些寶貝函式,對於多執行緒對全域性變數進行操作(自加、自減等)問題,我們就不用考慮執行緒鎖,可以考慮使用上述函式代替,和使用pthread_mutex保護的作用是一樣的,執行緒安全且效能上完爆執行緒鎖。 下面是對執行緒鎖和原子操作使用對比,並且進行效能測試與對比。程式碼來自於文獻【1】,弄懂後並稍微改動一點點。程式碼中分別給出加鎖、加執行緒鎖、原子計數操作三種情況的比較。
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <errno.h>
  5. #include <pthread.h>
  6. #include <sched.h>
  7. #include <linux/unistd.h>
  8. #include <sys/syscall.h>
  9. #include <linux/types.h>
  10. #include <time.h>
  11. #include <sys/time.h>
  12. #define INC_TO 1000000 // one million
  13. __u64 rdtsc ()  
  14. {  
  15.     __u32 lo, hi;  
  16.     __asm__ __volatile__  
  17.     (  
  18.        "rdtsc":"=a"(lo),"=d"(hi)  
  19.     );  
  20.     return (__u64)hi << 32 | lo;  
  21. }  
  22. int global_int = 0;  
  23. pthread_mutex_t count_lock = PTHREAD_MUTEX_INITIALIZER;//初始化互斥鎖
  24. pid_t gettid ()  
  25. {  
  26.     return syscall(__NR_gettid);  
  27. }  
  28. void * thread_routine1 (void *arg)  
  29. {  
  30.     int i;  
  31.     int proc_num = (int)(long)arg;  
  32.     __u64 begin, end;  
  33.     struct timeval tv_begin, tv_end;  
  34.     __u64 time_interval;  
  35.     cpu_set_t set;  
  36.     CPU_ZERO(&set);  
  37.     CPU_SET(proc_num, &set);  
  38.     if (sched_setaffinity(gettid(), sizeof(cpu_set_t), &set))  
  39.     {  
  40.         fprintf(stderr, "failed to set affinity\n");  
  41.         return NULL;  
  42.     }  
  43.     begin = rdtsc();  
  44.     gettimeofday(&tv_begin, NULL);  
  45.     for (i = 0; i < INC_TO; i++)  
  46.     {  
  47.         __sync_fetch_and_add(&global_int, 1);  
  48.     }  
  49.     gettimeofday(&tv_end, NULL);  
  50.     end = rdtsc();  
  51.     time_interval = (tv_end.tv_sec - tv_begin.tv_sec) * 1000000 + (tv_end.tv_usec - tv_begin.tv_usec);  
  52.     fprintf(stderr, "proc_num : %d, __sync_fetch_and_add cost %llu CPU cycle, cost %llu us\n", proc_num, end - begin, time_interval);  
  53.     return NULL;  
  54. }  
  55. void *thread_routine2(void *arg)  
  56. {  
  57.     int i;  
  58.     int proc_num = (int)(long)arg;  
  59.     __u64 begin, end;  
  60.     struct timeval tv_begin, tv_end;  
  61.     __u64 time_interval;  
  62.     cpu_set_t set;  
  63.     CPU_ZERO(&set);  
  64.     CPU_SET(proc_num, &set);  
  65.     if (sched_setaffinity(gettid(), sizeof(cpu_set_t), &set))  
  66.     {  
  67.         fprintf(stderr, "failed to set affinity\n");  
  68.         return NULL;  
  69.     }  
  70.     begin = rdtsc();  
  71.     gettimeofday(&tv_begin, NULL);  
  72.     for (i = 0; i < INC_TO; i++)  
  73.     {  
  74.         pthread_mutex_lock(&count_lock);  
  75.         global_int++;  
  76.         pthread_mutex_unlock(&count_lock);  
  77.     }  
  78.     gettimeofday(&tv_end, NULL);  
  79.     end = rdtsc();  
  80.     time_interval = (tv_end.tv_sec - tv_begin.tv_sec) * 1000000 + (tv_end.tv_usec - tv_begin.tv_usec);  
  81.     fprintf(stderr, "proc_num : %d, pthread_mutex_lock cost %llu CPU cycle, cost %llu us\n", proc_num, end - begin, time_interval);  
  82.     return NULL;    
  83. }  
  84. void *thread_routine3(void *arg)  
  85. {  
  86.     int i;  
  87.     int proc_num = (int)(long)arg;  
  88.     __u64 begin, end;  
  89.     struct timeval tv_begin, tv_end;  
  90.     __u64 time_interval;  
  91.     cpu_set_t set;  
  92.     CPU_ZERO(&set);  
  93.     CPU_SET(proc_num, &set);  
  94.     if (sched_setaffinity(gettid(), sizeof(cpu_set_t), &set))  
  95.     {  
  96.         fprintf(stderr, "failed to set affinity\n");  
  97.         return NULL;  
  98. 相關推薦

    Linux執行程式設計--原子計數操作__sync_fetch_and_add12操作

     最近自己做了一些涉及多執行緒程式設計的專案,其中就涉及到多執行緒間計數操作、共享狀態或者統計相關時間次數,這些都需要在多執行緒之間共享變數和修改變數,如此就需要在多執行緒間對該變數進行互斥操作和訪問。         通常遇到多執行緒互斥的問題,首先想到的就是

    執行程式設計

    https://en.wikipedia.org/wiki/Compare-and-swap   http://blog.chinaunix.net/uid-25424552-id-3772253.html   https://coolshell.cn/artic

    Linux C 執行程式設計互斥與條件變數

    一、互斥鎖互斥量從本質上說就是一把鎖, 提供對共享資源的保護訪問。  1. 初始化:  在Linux下, 執行緒的互斥量資料型別是pthread_mutex_t. 在使用前, 要對它進行初始化:  對於靜態分配的互斥量, 可以把它設定為PTHREAD_MUTEX_INITIA

    Java執行-

    1 無鎖類的原理詳解 1.1 CAS CAS演算法的過程是這樣:它包含3個引數CAS(V,E,N)。V表示要更新的變數,E表示預期值,N表示新值。僅當V 值等於E值時,才會將V的值設為N,如果V值和E值不同,則說明已經有其他執行緒做了更新,則當前執行緒什麼 都不做。最後,CAS返

    python3執行-執行同步

    - 1. 認識執行緒同步現象: 在https://blog.csdn.net/weixin_41827162/article/details/84104421執行緒非同步中, 將方法1中: 建多個執行緒,同時執行多個執行緒,由新到舊逐個釋放執行緒 改成: 建立一個執行緒,

    python3執行-執行非同步(推薦使用)

    - python3有threading和_thread兩種執行緒寫法,推薦使用threading。 開多執行緒就是為了使用多執行緒的非同步能力來同時執行多個執行緒。 1. threading方法 #!/usr/bin/python3 # 執行緒非同步 import thread

    Java執行初探

    Java的執行緒狀態 從作業系統的角度看,執行緒有5種狀態:建立, 就緒, 執行, 阻塞, 終止(結束)。如下圖所示   而Java定義的執行緒狀態有: 建立(New), 可執行(Runnable), 阻塞(Blocked), 等待(Waiting), 計時等待(Time

    Java定時任務Timer排程器 執行原始碼分析(圖文版)

      上一節通過一個小例子分析了Timer執行過程,牽涉的執行執行緒雖然只有兩個,但實際場景會比上面複雜一些。 首先通過一張簡單類圖(只列出簡單的依賴關係)看一下Timer暴露的介面。   為了演示Timer所暴露的介面,下面舉一個極端的例子(每一個介面方法面

    分類 - 執行

    專欄達人 授予成功建立個人部落格專欄

    JAVA執行造成的安全問題

    前言 執行緒可以看做我們每一個人,在社會中可能表現出不同的行為,所以人發生的情況執行緒也可能發生。   1.死鎖問題 兩個人吃飯,一雙筷子,一人拿起一根,等待前一個人丟下筷子;   2.飢餓問題 食堂吃飯需要排隊,還可以插隊,於是

    muduo執行伺服器的適用場合與程式設計模型

    文章目錄 一、程序與執行緒 1、程序的概念 2、關於程序的一個形象比喻(人) 3、執行緒的概念 二、多程序和多執行緒的適用場景 1、需要頻繁建立銷燬的優先用執行緒 2、

    執行佇列的實現

    一、什麼是多執行緒無鎖佇列? 多執行緒無鎖佇列還是有鎖的,只不過是用了cpu層面的CAS原子操作,用到這個操作,只需要在取佇列元素和新增佇列元素的時候利用CAS原子操作,就可以保證多個執行緒對佇列元素的有序存取; 二、什麼是CAS操作? CAS = Compare &am

    java執行批量拆分List匯入資料庫

    一、前言       前兩天做了一個匯入的功能,匯入開始的時候非常慢,匯入2w條資料要1分多鐘,後來一點一點的優化,從直接把list懟進Mysql中,到分配把list匯入Mysql中,到多執行緒把list匯入Mysql中。時間是一點一點的變少了。非常的爽,最後

    Java執行基礎

    執行緒的狀態 狀態名稱 說明 new 初始狀態:執行緒被構建,但沒有呼叫start()方法 runnable 執行狀態:就緒和執行統稱“執行中” blocked 阻塞狀態:執行緒阻塞於鎖 waitin

    OpenMP執行計算過程中任務排程問題

    對於OpenMP的任務排程主要針對於並行的for迴圈,當每一次迴圈過程中的計算時間複雜度不一致的時候,簡單的給每一個執行緒分配相同次數的迭代,會導致執行緒計算負載不均衡。不僅如此,對於實時計算的計算機,每一個核心的佔用率是不一樣的。針對該問題,OpenMP中給出

    併發執行程式設計中條件變數和虛假喚醒的討論

    轉自:http://blog.csdn.net/puncha/article/details/8493862 From: http://siwind.iteye.com/blog/1469216 From:http://en.wikipedia.org/wiki/S

    Java執行系列05(執行等待與喚醒)

    1、wait(),notify(),notifyAll()等方法介紹 在Object.java中,定義了wait(), notify()和notifyAll()等介面。wait()的作用是讓當前執行緒進入等待狀態,同時,wait()也會讓當前執行緒釋放它

    Java執行系列(三)之阻塞執行的多種方法

    前言: 在某些應用場景下,我們可能需要等待某個執行緒執行完畢,然後才能進行後續的操作。也就是說,主執行緒需要等待子執行緒都執行完畢才能執行後續的任務。 例如,當你在計算利用多執行緒執行幾個比較耗時的任務的時候,主執行緒需要利用這幾個執行緒計算的結果,才能進行後

    JAVA執行之記憶體可見性

                                        多執行緒之記憶體可見性 一、什麼是可見性? 一個執行緒對共享變數值的修改,能夠及時地被其他執行緒所看到。 共享變數:如果一個變數在多個執行緒的工作記憶體中都存在副本,那麼這個變數就是這幾個執行緒的共

    執行原子操作和記憶體柵欄(二)

            這裡記錄下各種鎖的使用和使用場景,在多執行緒場景開發時,我們經常遇到多個執行緒同時讀寫一塊資源爭搶一塊資源的情況,比如同時讀寫同一個欄位屬性,同時對某個集合進行增刪改查,同時對資料庫進行讀寫(這裡