__sync_fetch_and_add函式(Redis原始碼學習)
__sync_fetch_and_add函式(Redis原始碼學習)
在學習redis-3.0原始碼中的sds檔案時,看到裡面有如下的C程式碼,之前從未接觸過,所以為了全面學習redis原始碼,追根溯源,學習一下__sync_fetch_and_add的系列函式:
#define update_zmalloc_stat_add(__n) __sync_add_and_fetch(&used_memory, (__n))
在網上查詢相關 __sync_add_and_fetch 函式的知識點,基本都是一樣的內容,於是總結如下。
1.背景由來
實現多執行緒環境下的計數器操作,統計相關事件的次數. 當然我們知道,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,
2.系列函式
__sync_fetch_and_add系列一共有十二個函式,有加/減/與/或/異或/等函式的原子性操作函式,__sync_fetch_and_add,顧名思義,先fetch,然後自加,返回的是自加以前的值。以count = 4為例,呼叫__sync_fetch_and_add(&count,1)之後,返回值是4,然後,count變成了5.
簡單驗證程式碼如下sync_fetch_add.c:
#include <stdio.h> #include <stdlib.h> int main(int argc, char **argv){ int count = 4; printf("111 count:%d\n",count); int retval = __sync_fetch_and_add(&count,10); printf("222 retval:%d\n",retval); printf("222 count:%d\n",count); return 0; }
linux 系統中命令列執行:gdb -g -o sync_fetch_add sync_fetch_add.c
得到可執行檔案,執行後得到如下結果:
./sync_fetch_add
111 count:4
222 retval:4
222 count:14
其他函式可以自行驗證。
有__sync_fetch_and_add,自然也就有__sync_add_and_fetch,呵呵這個的意思就很清楚了,先自加,在返回。他們的關係與i++和++i的關係是一樣的。有了這個函式,對於多執行緒對全域性變數進行自加,我們就再也不用理執行緒鎖了。下面這行程式碼,和上面被pthread_mutex保護的那行程式碼作用是一樣的,而且也是執行緒安全的。
在用gcc編譯的時候要加上選項 -march=i686,我在執行上面程式碼時,gcc沒加該引數,使用到的版本gcc version 4.4.7 20120313 , 上面程式碼能正常執行通過。
下面是這群函式的全部,無非是先fetch再運算,或者先運算再fetch。
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);
GCC 提供的原子操作
gcc從4.1.2提供了__sync_*系列的built-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, ...)
這兩組函式的區別在於第一組返回更新前的值,第二組返回更新後的值。
看網上有大師的程式碼測試例子Alexander Sandler,現拷貝為 sync_fetch2.c 檔案如下並驗證執行結果:
#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>
#define INC_TO 1000000 // one million...
int global_int = 0;
pid_t gettid( void )
{
return syscall( __NR_gettid );
}
void *thread_routine( void *arg )
{
int i;
int proc_num = (int)(long)arg;
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;
}
for (i = 0; i < INC_TO; i++)
{
// global_int++;
__sync_fetch_and_add( &global_int, 1 );
}
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 = (pthread_t *)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;
}
上面程式碼在RHEL6.9中編譯:g++ -g -o sync_fetch2 sync_fetch2.c -lpthread
執行結果為:
./sync_fetch2
Starting 4 threads...
After doing all the math, global_int value is: 4000000
Expected value is: 4000000
如果將上面thread_routine函式中的這兩句換一下,直接用變數加加,則每次執行都得到不一樣的值
global_int++;
// __sync_fetch_and_add( &global_int, 1 );
修改後得到結果如下:
$./sync_fetch2
Starting 4 threads...
After doing all the math, global_int value is: 1428371
Expected value is: 4000000
$ ./sync_fetch2
Starting 4 threads...
After doing all the math, global_int value is: 2479197
Expected value is: 4000000
3.小結
可以從程式碼驗證中看到 __sync_fetch_and_add 函式的作用,在多執行緒中,對簡單的變數運算能保證結果的正確,至於其他函式,參考上面程式碼,讀者可以自行驗證。
另外基於上面例子,有人修改程式碼,加上執行消耗時間,通過__sync_fetch_and_add和加鎖機制的對比,發現__sync_fetch_and_add比加解鎖機制快了6-7倍,執行速度還是很快的,因為涉及到彙編程式碼,後續有機會會再學習驗證。
本人才疏學淺,錯誤不當之處,請批評指正。
如果文章對您有一點點用處,我會很高興能幫到您。多謝關注推薦和轉發,謝謝!
參考網址:
http://www.alexonlinux.com/multithreaded-simple-data-type-access-and-atomic-variables
https://blog.csdn.net/i_am_jojo/article/details/7591743
https://www.zhihu.com/question/280022939
https://blog.csdn.net/long2324066440/article/details/72784084