假设存在这样一个情况:需要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 LOCKLESS pthread_mutex_t mutex_lock; #endif static 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); #else pthread_mutex_lock(&mutex_lock); count++; pthread_mutex_unlock(&mutex_lock); #endif #else count++; #endif } return NULL; } int main(int argc, const char *argv[]) { pthread_t thread_ids[THREAD_NUM]; int i = 0; #ifndef LOCKLESS pthread_mutex_init(&mutex_lock, NULL); #endif for(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=gcc CFLAGS= -Wall LIB= -lpthread OBJS=gcc_sync_test.o SRCS=${OBJS:%.o=%.c} TARGETS=.depend gcc_sync_test all:$(TARGETS) .depend: @$(CC) $(CFLAGS) -MM $(SRCS) > .depend -include .depend gcc_sync_test: $(OBJS) $(CC) $(CFLAGS) $^ $(LIB) -o $@ @echo $@ > .gitignore clean: 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)
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宏),似乎也能得到正确的结果。