轉自:http://blog.csdn.net/minCrazy/article/details/40791795
多線程間計數操作、共享狀態或者統計相關時間次數,這些都需要在多線程之間共享變量和修改變量,如此就需要在多線程間對該變量進行互斥操作和訪問。
通常遇到多線程互斥的問題,首先想到的就是加鎖lock,通過加互斥鎖來進行線程間互斥,但是最近有看一些開源的項目,看到有一些同步讀和操作的原子操作函數——__sync_fetch_and_add系列的命令,然后自己去網上查找一番,找到一篇博文有介紹這系列函數,學習一番后記錄下來。
首先,C/C++程序中count++這種操作不是原子的,一個自加操作,本質上分為3步:
- 從緩存取到寄存器
- 在寄存器內加1
- 再存入緩存
通常,最簡單的方法就是加鎖保護,互斥鎖(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;
第二個函數在返回操作之前的值。
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- #include <sched.h>
- #include <linux/unistd.h>
- #include <sys/syscall.h>
- #include <linux/types.h>
- #include <time.h>
- #include <sys/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 ()
- {
- return syscall(__NR_gettid);
- }
- void * thread_routine1 (void *arg)
- {
- int i;
- int proc_num = (int)(long)arg;
- __u64 begin, end;
- struct timeval tv_begin, tv_end;
- __u64 time_interval;
- cpu_set_t set;
- CPU_ZERO(&set);
- CPU_SET(proc_num, &set);
- if (sched_setaffinity(gettid(), sizeof(cpu_set_t), &set))
- {
- fprintf(stderr, "failed to set affinity\n");
- return NULL;
- }
- begin = rdtsc();
- gettimeofday(&tv_begin, NULL);
- for (i = 0; i < INC_TO; i++)
- {
- __sync_fetch_and_add(&global_int, 1);
- }
- gettimeofday(&tv_end, NULL);
- end = rdtsc();
- time_interval = (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, time_interval);
- 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 time_interval;
- cpu_set_t set;
- CPU_ZERO(&set);
- CPU_SET(proc_num, &set);
- if (sched_setaffinity(gettid(), sizeof(cpu_set_t), &set))
- {
- fprintf(stderr, "failed to set affinity\n");
- 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();
- time_interval = (tv_end.tv_sec - tv_begin.tv_sec) * 1000000 + (tv_end.tv_usec - tv_begin.tv_usec);
- fprintf(stderr, "proc_num : %d, pthread_mutex_lock cost %llu CPU cycle, cost %llu us\n", proc_num, end - begin, time_interval);
- return NULL;
- }
- void *thread_routine3(void *arg)
- {
- int i;
- int proc_num = (int)(long)arg;
- __u64 begin, end;
- struct timeval tv_begin, tv_end;
- __u64 time_interval;
- cpu_set_t set;
- CPU_ZERO(&set);
- CPU_SET(proc_num, &set);
- if (sched_setaffinity(gettid(), sizeof(cpu_set_t), &set))
- {
- fprintf(stderr, "failed to set affinity\n");
- return NULL;
- }
- begin = rdtsc();
- gettimeofday(&tv_begin, NULL);
- for (i = 0; i < INC_TO; i++)
- {
- global_int++;
- }
- gettimeofday(&tv_end, NULL);
- end = rdtsc();
- time_interval = (tv_end.tv_sec - tv_begin.tv_sec) * 1000000 + (tv_end.tv_usec - tv_begin.tv_usec);
- fprintf(stderr, "proc_num : %d, no lock cost %llu CPU cycle, cost %llu us\n", proc_num, end - begin, time_interval);
- return NULL;
- }
- int main()
- {
- int procs = 0;
- int all_cores = 0;
- int i;
- pthread_t *thrs;
- procs = (int)sysconf(_SC_NPROCESSORS_ONLN);
- if (procs < 0)
- {
- fprintf(stderr, "failed to fetch available CPUs(Cores)\n");
- return -1;
- }
- all_cores = (int)sysconf(_SC_NPROCESSORS_CONF);
- if (all_cores < 0)
- {
- fprintf(stderr, "failed to fetch system configure CPUs(Cores)\n");
- return -1;
- }
- printf("system configure CPUs(Cores): %d\n", all_cores);
- printf("system available CPUs(Cores): %d\n", procs);
- thrs = (pthread_t *)malloc(sizeof(pthread_t) * procs);
- if (thrs == NULL)
- {
- fprintf(stderr, "failed to malloc pthread array\n");
- return -1;
- }
- printf("starting %d threads...\n", procs);
- for (i = 0; i < procs; i++)
- {
- if (pthread_create(&thrs[i], NULL, thread_routine1, (void *)(long) i))
- {
- fprintf(stderr, "failed to pthread create\n");
- procs = i;
- break;
- }
- }
- for (i = 0; i < procs; i++)
- {
- pthread_join(thrs[i], NULL);
- }
- printf("after doing all the math, global_int value is: %d\n", global_int);
- printf("expected value is: %d\n", INC_TO * procs);
- free (thrs);
- return 0;
- }
- system configure CPUs(Cores): 8
- system available CPUs(Cores): 8
- starting 8 threads...
- proc_num : 5, no lock cost 158839371 CPU cycle, cost 66253 us
- proc_num : 6, no lock cost 163866879 CPU cycle, cost 68351 us
- proc_num : 2, no lock cost 173866203 CPU cycle, cost 72521 us
- proc_num : 7, no lock cost 181006344 CPU cycle, cost 75500 us
- proc_num : 1, no lock cost 186387174 CPU cycle, cost 77728 us
- proc_num : 0, no lock cost 186698304 CPU cycle, cost 77874 us
- proc_num : 3, no lock cost 196089462 CPU cycle, cost 81790 us
- proc_num : 4, no lock cost 200366793 CPU cycle, cost 83576 us
- after doing all the math, global_int value is: 1743884
- expected value is: 8000000
線程鎖下運行結果:
- system configure CPUs(Cores): 8
- system available CPUs(Cores): 8
- starting 8 threads...
- proc_num : 1, pthread_mutex_lock cost 9752929875 CPU cycle, cost 4068121 us
- proc_num : 5, pthread_mutex_lock cost 10038570354 CPU cycle, cost 4187272 us
- proc_num : 7, pthread_mutex_lock cost 10041209091 CPU cycle, cost 4188374 us
- proc_num : 0, pthread_mutex_lock cost 10044102546 CPU cycle, cost 4189546 us
- proc_num : 6, pthread_mutex_lock cost 10113533973 CPU cycle, cost 4218541 us
- proc_num : 4, pthread_mutex_lock cost 10117540197 CPU cycle, cost 4220212 us
- proc_num : 3, pthread_mutex_lock cost 10160384391 CPU cycle, cost 4238083 us
- proc_num : 2, pthread_mutex_lock cost 10164464784 CPU cycle, cost 4239778 us
- after doing all the math, global_int value is: 8000000
- expected value is: 8000000
原子操作__sync_fetch_and_add下運行結果:
- system configure CPUs(Cores): 8
- system available CPUs(Cores): 8
- starting 8 threads...
- proc_num : 3, __sync_fetch_and_add cost 2364148575 CPU cycle, cost 986129 us
- proc_num : 1, __sync_fetch_and_add cost 2374990974 CPU cycle, cost 990652 us
- proc_num : 2, __sync_fetch_and_add cost 2457930267 CPU cycle, cost 1025247 us
- proc_num : 5, __sync_fetch_and_add cost 2463027030 CPU cycle, cost 1027373 us
- proc_num : 7, __sync_fetch_and_add cost 2532240981 CPU cycle, cost 1056244 us
- proc_num : 4, __sync_fetch_and_add cost 2555055054 CPU cycle, cost 1065760 us
- proc_num : 0, __sync_fetch_and_add cost 2561248971 CPU cycle, cost 1068331 us
- proc_num : 6, __sync_fetch_and_add cost 2558781396 CPU cycle, cost 1067314 us
- after doing all the math, global_int value is: 8000000
- expected value is: 8000000
測試結果表明,正確結果為8000000,而實際為1743884。表明多線程下修改全局計數,不加鎖的話是錯誤的;
2. 加鎖情況下,無論是線程鎖還是原子性操作,均可獲得正確結果。
3. 性能上__sync_fetch_and_add()完爆線程鎖。
從性能測試結果上看,__sync_fetch_and_add()速度大致是線程鎖的4-5倍。
類型 | 平均CPU周期(circle) | 平均耗時(us) |
---|---|---|
不加鎖 | 180890066 | 75449.13 |
線程鎖 | 10054091901 | 4193740.875 |
原子操作 | 2483427906 | 1035881.25 |
注:如上的性能測試結果,表明__sync_fetch_and_add()速度大致是線程鎖的4-5倍,而並非文獻【1】中6-7倍。由此,懷疑可能是由不同機器、不同CPU導致的,上述測試是在一台8core的虛擬機上實驗的。為此,我又在不同的機器上重復相同的測試。
24cores實體機測試結果,表明__sync_fetch_and_add()速度大致只有線程鎖的2-3倍。
類型 | 平均CPU周期(circle) | 平均耗時(us) |
---|---|---|
不加鎖 | 535457026 | 233310.5 |
線程鎖 | 9331915480 | 4066156.667 |
原子操作 | 3769900795 | 1643463.625 |
總體看來,原子操作__sync_fetch_and_add()大大的優於線程鎖。
另外:
上面介紹的原子操作參數里都有可擴展參數(...)用來指出哪些變量需要memory barrier,因為目前gcc實現的是full barrier(類似Linux kernel中的mb(),表示這個操作之前的所有內存操作不會被重排到這個操作之后),所以可以忽略掉這個參數。下面是有關memory barrier的東西。
關於memory barrier, cpu會對我們的指令進行排序,一般說來會提高程序的效率,但有時候可能造成我們不希望看到的結果。舉例說明,比如我們有一硬件設備,當你發出一個操作指令的時候,一個寄存器存的是你的操作指令(READ),兩個寄存器存的是參數(比如地址和size),最后一個寄存器是控制寄存器,在所有的參數都設置好后向其發出指令,設備開始讀取參數,執行命令,程序可能如下:
write1(dev.register_size, size);
write1(dev.register_addr, addr);
write1(dev.register_cmd, READ);
write1(dev.register_control, Go);
write1(dev.register_addr, addr);
write1(dev.register_cmd, READ);
write1(dev.register_control, GO);