1. 程式人生 > 其它 >__sync_fetch_and_add函式(Redis原始碼學習)

__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