并发下的技术方案(互斥锁、自旋锁、原子操作)

	场景:车站里有很多的车票。很多人来抢购车票。

上面场景中车站所拥有的车票,是所有用户共同操作的资源。我们把这种资源称为临界资源。在并发条件下,对临界资源的操作,经常会出现非原子变量赋值出现错误。原因如下:

  • 非原子赋值大多数都不是一条指令能完成的。
  • 线程1将临界资源中的非原子变量改到一半,线程2正好就来读取了这个半成品变量,并基于此去做计算,可能会造成错误。

举例

   有一个池子number,十台机器。每个机器会一个一个球地往池子里放球,十个机器同时往池子里放球。
   各机器各放一万个球。

不熟悉pthread操作的可以看文末的注解。1
代码如下:


#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define thread_count 10

//对线程操作的函数。每个线程对number进行加10000
void* number_count(void *number){
	int* pnum = (int *)number;
	for(int i = 0;i < 10000; i++){
		//这个赋值操作是关键
		(*pnum) ++;
		usleep(1);
	}
}

int main(){
	// 初始化线程id
	pthread_t thread_id[thread_count] = {0};
	// number是临界资源
	int number =  0;
	//创建十条线程共同操作number这个临界资源
	for(int i = 0;i < thread_count; i++){
		pthread_create(&thread_id[i],NULL,&number_count,&number);
	}
	//每隔一秒打印一次number的值
	for(int j = 0;j < 100; j++){
		printf("number:%d\n",number);
		sleep(1);
	}
}

以上代码不考虑非原子操作,最后number应该是能加到十万的。然而结果如下:
在这里插入图片描述
最后number并没有加到十万。为什么呢???

临界资源的操作

上例中的临界资源是number。其中语句(*pnum)++是非原子变量赋值。非原子变量赋值的意思是这个语句虽然是一句,但是线程实际操作的时候是被拆分多个原子语句执行的。这里的(*pnum)++便可拆分为以下原子语句

// eax为寄存器。先把number的数给寄存器eax,
// 然后eax自增1.最后把eax的值给number。
mov [number],eax;
inc eax;
mov eax,[number];

我们之前预想的情况是一个线程执行完,另一个线程再执行。这样临界资源就能加到十万了。但是实际情况是总有多个线程同时操作临界资源。那就会有如下结果:

// 此时线程1在操作临界资源。number为100。
mov [number],eax;
inc eax;
// 线程1还没运行完。线程2也开始操作number
// 线程2此时的number也是为100
mov [number],eax;
inc eax;
mov eax,[number];// 线程2操作后的number为101
//下面的语句是线程1的语句。此时寄存器内的值为101
mov eax,[number];// 线程1操作后的number也为101

如上情况,两个线程都操作了number,但是只加了1。最后导致结果的错误。那如何解决这个问题呢?

  • 一是,我们可以把这几个原子语句“捆绑”起来。就是我这几个语句还没执行完,其他的线程就先别执行了。
  • 二是,我们直接把这个非原子语句。直接转化为原子语句。

互斥锁(mutex)

互斥锁能实现将几个原子语句“捆绑”起来的效果。当我们在这个原子语句前加个互斥锁后 ,当一个线程在操作这个临界资源的时候 ,其他线程检测到这个临界资源是关锁的状态,其他线程会切换成等待,等待下次被调用 。也就是在同一时刻,只有一个线程能操作这个临界资源。
使用互斥锁的场景:锁的内容比较多的场景 。如:线程安全的rbtree,添加可以使用mutex。
代码如下:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>


#define thread_count 10
#define INFO printf
//定义一把互斥锁
pthread_mutex_t mutex;

void* number_count(void *number){
	int* pnum = (int *)number;
	for(int i = 0;i < 10000; i++){
		#if 0
		(*pnum) ++;
		#else
			// 使用互斥锁
			pthread_mutex_lock(&mutex);
			(*pnum)++;
			// 开锁
			pthread_mutex_unlock(&mutex);
		#endif
		usleep(1);
	}
}

int main(){
	// 初始化线程id
	pthread_t thread_id[thread_count] = {0};
	//初始化互斥锁
	pthread_mutex_init(&mutex,NULL);
	int number =  0;
	for(int i = 0;i < thread_count; i++){
		pthread_create(&thread_id[i],NULL,&number_count,&number);
	}
	for(int j = 0;j < 100; j++){
		INFO("number:%d\n",number);
		sleep(1);
	}
}

结果如下:
在这里插入图片描述
从结果可知,number成功加到了十万。也说明了互斥锁(mutex)的有效性。

自旋锁

自旋锁,跟互斥锁功能类似。也能实现多个原子语句的捆绑。其原理是当临界资源被锁后,其他线程会不断地检测临界资源是否开锁,相当于while(1);
好处:可以很快地获得到锁,效率较高。
坏处:因为在不断地检测。如果等待的时间较长,很浪费cpu的资源 。
适合的场景:锁的内容较少的场景
代码如下 :


#include <stdio.h>
#include <pthread.h>
#include <unistd.h>


#define thread_count 10
#define INFO printf
//定义一把互斥锁
pthread_mutex_t mutex;
//定义一把自旋锁
pthread_spinlock_t spin;

void* number_count(void *number){
	int* pnum = (int *)number;
	for(int i = 0;i < 10000; i++){
		#if 0
		(*pnum) ++;
		#elif 0
			//使用互斥锁
			pthread_mutex_lock(&mutex);
			(*pnum)++;
			pthread_mutex_unlock(&mutex);
		#else
			//使用自旋锁
			pthread_spin_lock(&spin);
			(*pnum)++;
			pthread_spin_unlock(&spin);
		#endif
		usleep(1);
	}
	
}

int main(){
	pthread_t thread_id[thread_count] = {0};// 这里要注意初始化
	//初始化互斥锁
	pthread_mutex_init(&mutex,NULL);
	//初始化自旋锁
	pthread_spin_init(&spin,PTHREAD_PROCESS_SHARED);
	int number =  0;
	for(int i = 0;i < thread_count; i++){
		pthread_create(&thread_id[i],NULL,&number_count,&number);
	}
	for(int j = 0;j < 100; j++){
		INFO("number:%d\n",number);
		sleep(1);
	}
}

原子操作

原子操作指的是一个或者一系列不可拆分的操作。也就是不会被线程调度机制打断的操作,运行期间不会发生任何的上下文切换。使用原子操作可以将多个指令转化为一个指令,使其没有被分割的可能。
例子的代码实现:


#include <stdio.h>
#include <pthread.h>
#include <unistd.h>


#define thread_count 10
#define INFO printf

int inc(int *count,int step){
	
	int old;
	__asm__ volatile(
		"lock; xaddl %2, %1;"
		: "=a" (old)
		: "m" (*count),"a"(step)
		: "cc", "memory"
	);
	return old;
}

void* number_count(void *number){
	int* pnum = (int *)number;
	for(int i = 0;i < 10000; i++){
		#if 0
		(*pnum) ++;
		#else
			inc(number,1);
		#endif
		usleep(1);
	}
}

int main(){
	pthread_t thread_id[thread_count] = {0};
	int number =  0;
	for(int i = 0;i < thread_count; i++){
		pthread_create(&thread_id[i],NULL,&number_count,&number);
	}
	for(int j = 0;j < 100; j++){
		INFO("number:%d\n",number);
		sleep(1);
	}
}

汇编指令解释,请参考注解链接2


  1. pthread的相关知识 ↩︎

  2. http://t.csdn.cn/IT6Vp ↩︎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值