最近編碼須要實現多線程環境下的計數器操做,統計相關事件的次數。下面是一些學習心得和體會。不敢妄稱原創,基本是學習筆記。遇到相關的引用,我會致謝。
固然咱們知道,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 linux
__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);
須要說起的是,這個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> ide
#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倍