无锁编程:最简单例子

场景

    假设存在这样一个情况:需要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)

其中,“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宏),似乎也能得到正确的结果。


你可能感兴趣的:(无锁编程:最简单例子)