情境
假設存在這樣一個情況:需要N個線程對一個全域的變數進行M次遞增操作。首先想到的常常是,使用互斥量。當然在“無鎖”的世界裡,還有其它實現方式。話不多說,看代碼:
測試代碼
gcc_sync_test.c
#include <stdio.h>#include <stdlib.h>#include <pthread.h>#define TEST_ROUND 20000#define THREAD_NUM 10#define SYNC#define LOCKLESS#ifndef LOCKLESSpthread_mutex_t mutex_lock;#endifstatic volatile int count = 0;void *test_func(void *arg){int i = 0;for(i = 0; i < TEST_ROUND; i++){#ifdef SYNC#ifdef LOCKLESS__sync_fetch_and_add(&count, 1);#elsepthread_mutex_lock(&mutex_lock);count++;pthread_mutex_unlock(&mutex_lock);#endif#elsecount++;#endif}return NULL;}int main(int argc, const char *argv[]){pthread_t thread_ids[THREAD_NUM];int i = 0;#ifndef LOCKLESSpthread_mutex_init(&mutex_lock, NULL);#endiffor(i = 0; i < sizeof(thread_ids)/sizeof(pthread_t); i++){pthread_create(&thread_ids[i], NULL, test_func, NULL);}for(i = 0; i < sizeof(thread_ids)/sizeof(pthread_t); i++){pthread_join(thread_ids[i], NULL);}printf("count=%d\r\n", count);return 0;}
Makefile
CC=gccCFLAGS= -WallLIB= -lpthreadOBJS=gcc_sync_test.o SRCS=${OBJS:%.o=%.c}TARGETS=.depend gcc_sync_testall:$(TARGETS).depend:@$(CC) $(CFLAGS) -MM $(SRCS) > .depend -include .dependgcc_sync_test: $(OBJS)$(CC) $(CFLAGS) $^ $(LIB) -o $@@echo $@ > .gitignoreclean:rm -rf $(OBJS) $(TARGETS).c.o:$(CC) $(CFLAGS) -c $< -o $@
測試結果
在原始碼檔案gcc_sync_test.c中,使用SYNC宏來控制是否啟用線程間同步;在啟用SYNC情況下,使用LOCKLESS宏來控制是否使用“無鎖”方式,還是使用互斥量方式。
選擇“無鎖”方式,編譯、運行程式可以得到正確的結果“count=200000”,這和使用互斥量方式得到的結果一樣。如果感興趣的話,可以試試不採用任何一種同步方案(即,注釋掉SYNC宏的定義),可以發現輸出的結果是不正確的,道理很顯然。
結果分析
那麼,為什麼不用phtread_mutex_lock也可以實現線程間同步?可以看到程式中使用了__sync_fetch_and_add在實現加法運算。__sync_fetch_and_add是GCC內建的原子操作,它的原理《GCC內建的原子操作》中已經做了簡單的敘述。如果關注GCC是如何?該原子操作的,可以通過產生彙編代碼的方式來探究。
gcc -S gcc_sync_test.c
產生彙編代碼gcc_sync_test.s。查看它,可以發現其中有如下代碼:
jmp .L2.L3: lock addl $1, count(%rip) addl $1, -4(%rbp).L2: cmpl $19999, -4(%rbp)
其中,“lock addl $1, count(%rip)”中的lock既是關鍵所在。lock是一個指令首碼,Intel的手冊上對其的解釋是:
Causes the processor's LOCK# signal to be asserted during execution of the accompanying instruction (turns the instruction into an atomic instruction). In a multiprocessor environment, the LOCK# signal insures that the processor has exclusive use of any shared memory while the signal is asserted.
如果不使用原子操作__sync_fetch_and_add,直接進行count++的話,產生的彙編代碼大致是這樣的:
jmp .L2 .L3: movl count(%rip), %eax addl $1, %eax movl %eax, count(%rip) addl $1, -4(%rbp).L2: cmpl $19999, -4(%rbp)
顯然缺了lock指令首碼。
其它
至於,為什麼count變數要是volatile的,這是避免使用gcc最佳化選項後直接將M此迴圈的結果算出,影響了執行個體代碼的顯著性。讀者可以自己嘗試一下:去掉volatile修飾,gcc編譯時間使用-O2最佳化,不使用任何同步的情況下(不啟用SYNC宏),似乎也能得到正確的結果。