图示并发线程中的锁问题

最近在看“深入计算机操作系统”的并发编程部分,之前了解这一块都是从java的层面来看的。比如synchronized偏向锁,lock监视器实现锁,CAS无锁结构。相信做java的童鞋对这些都很熟悉,但是对于底层锁的实现,代码锁的范围还是有些模糊。似是而非的感觉,这次就跟着汤汤一起深入汇编代码的层面来揭开这层神秘的面纱。

在编写并发代码的时候主要面临的是共享变量的同步问题
本文中会用一段简短的程序来进行验证实验。使用进度图中的不安全区来确定锁的范围。锁范围的使用也是很重要的调优点呐。

进度图确定锁范围

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

//线程运行主体
void *thread(void *vargp);

/* Global shared variable */
volatile long cnt = 0; /* Counter */

int main(int argc, char const *argv[])
{
    
    
	long niters;
	pthread_t tid1, tid2; //线程id

	// 检查输入参数
	if (argc != 2){
    
    
		printf("usage: %s <niters>\n", argv[0]);
		exit(0);
	}

	niters = atoi(argv[1]);

	//创建两个独立线程并join堵塞在主线程中
	pthread_create(&tid1, NULL, thread, &niters);
	pthread_create(&tid2, NULL, thread, &niters);
	pthread_join(tid1, NULL);
	pthread_join(tid2, NULL);

	//检查运行结果
	if (cnt != (2 * niters))
		printf("BOOM! cnt=%ld\n", cnt);
	else
		printf("OK cnt=%ld\n", cnt);
	return 0;
}

void *thread(void *vargp)
{
    
    
	long i, niters = *((long *)vargp);
	for (i = 0; i < niters; i++)
	{
    
    
		cnt++;
	}
	return NULL;
}

上面代码中创建了两个线程,每个线程都对共享计数变量cnt加1。程序的处理逻辑很简单,每个创建的线程都会计数器cnt增加了niters次,那么最终的输出值应该是2*niters。将代码放到centos7上面运行不仅得出了错误的结果并且每一次的结果还都不相同。
在这里插入图片描述
这里我们为了更深入的了解问题,将线程主题内循环那部分的汇编代码单独拎出来看:
在这里插入图片描述
这里我们来解读下汇编代码的五部分:

  • Hi:在循环头部的指令块。
  • Li:加载共享变量cnt到累加寄存器%rdxi的指令,这里的%rdxi表示线程i中寄存器rdx的值。
  • Ui:更新(增加)%rdxi的指令
  • Si:将%rdxi的更新值存回到共享变量cnt的指令。
  • Ti:循环尾部的指令块。

其中Hi和Ti两部分在并发线程中是没有问题的,但是Li,Ui和Si三部分如果在线程中交替运行共享变量是会出现问题的。
比如线程i加载cnt=3 到寄存器中,进行累加cnt++,此时还没有执行Si存储指令。线程j开始执行Li加载操作cnt=3,然后进行累加Ui,Si存储指令将cnt的值设置为4。之后线程i执行存储Si操作。这样的执行顺序就会造成连续两次累加却只加了1

我们这里我们将每个线程的指令执行在坐标上顺序标出,线程i和j构成一个二位坐标,其中的不安全区便是我们加锁共享变量的区域:
在这里插入图片描述

信号量实现来实现互斥

这种方法是基于一种信号量的特殊类型变量的。信号量s是具有非负整数值的全局变量,只能由两种特殊的操作来处理,称为sem_wait和sem_post。

  • sem_wait:如果s是非零的,那么sem_wait将s减1,并且立即返回。如果s为零,那么就挂起这个线程。直到s变为非零,而一个sem_post会重启这个线程。在重启之后,sem_wait将s减1,并将控制返回给调用者。
  • sem_post:sem_post操作会将s加1。如果有任何线程阻塞在sem_wait操作等待s编程非零,那么sem_post操作会重启这些线程中的一个,然后该线程将s减1,完成他的sem_wait操作。

注意: sem_wait和sem_post都是原子性操作,不可分割。所以没有线程安全问题。sem_post必须只能重启一个正在等待的线程,如果有多个线程在等待,将会随机重启一个。

至此我们信号量来同步共享变量就能实现了。首先声明一个信号量mutex:

volatile long cnt = 0;
sem_t mutex;

然后在main函数中将mutex初始化为1:

sem_init(&mutex, 0, 1)

最后我们在线程循环主题中对共享变量cnt加上sem_wait和sem_post操作,实现对它们的保护:

	for (i = 0; i < niters; i++)
	{
    
    
		sem_wait(&mutex);
		cnt++;
		sem_post(&mutex);
	}

再次运行同步后的程序,就能输出正确的结果啦。
在这里插入图片描述
如果问题,欢迎大家留言讨论。
参考书籍:深入计算机操作系统

猜你喜欢

转载自blog.csdn.net/weixin_42662358/article/details/109700096
今日推荐