操作系统:上锁了就一定安全吗


前言

在操作系统中,针对并发,我们往往采用对临界资源上锁的方式保证数据能够按照我们预期的顺序得以访问,但在实际情况中,上了锁就一定能够保证线程安全,保证我们线程所需要的资源按照我们预期访问吗?

一、线程的构成

在解答以上问题之前,我们先复习一下线程的组成部分,一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈构成。其中堆上的数据为线程间共享,寄存器内的数据为线程私有。

二、编译器优化导致的上锁未达到预期

1.线程寄存器存储值未写回导致的上锁未达到预期

来看以下一段伪码

x=0
Thread1{
lock();
x++;
unlock();
}

Thread2{
lock();
x++;
unlock();
}

注:这里重点考虑线程寄存器内存储值的影响,不考虑x++;这一指令的非原子性对线程并发产生的影响。

表明上看这里我们添加了lock();和unlock();语句保证x在一段时间内只能由一个进程访问,这确实是事实。我们最终想要达到的是经过两个线程的执行,x的值从0变为2,但事实上x的值最终一定为2吗。
事实上,线程在执行的过程中,编译器为了提高线程读取的值的访问速度,可以将线程读取的值存储在线程寄存器中,而线程间的线程寄存器内的值是独立的,彼此之间不能进行访问,就有可能出现以下结果。

  • [Thread1]读取x的值存储到自己的寄存器R[1]中(R[1]=0)
  • [Thread1]R[1]++(R[1]=1,但是此时Thread1考虑到可能还要访问x,为提高访问速度,Thread1并未将x的值写回,而是仍旧存储在寄存器中)
  • [Thread2]读取x的值存储到自己的寄存器R[2]中(R[2]=0)
  • [Thread2]R[2]++(R[2]=1,写回x,此时x=1)
  • [Thread1] (很久以后)将R1寄存器中的值写回到x,x的值仍旧为1

从以上我们可以看到,即使加了锁,因为编译器要提高访问速度将某个值存储到线程的寄存器中,也有可能保证不了多线程的安全。

2.编译器动态调度指令导致的上锁未达到预期

事实上,编译器动态调度指令——编译器为提高效率而交换指令的先后执行顺序,也有可能导致交换两条相邻指令。
如以下的例子:

x=y=0
Thread1{
x=1;
r1=y;
}

Thread2{
y=1;
r2=x;
}

正常情况,若按照代码顺序执行,不管如何并发,最后得到的结果总归是r1=r2=1;
但若是编译器动态调整了指令顺序,使得指令执行的顺序变为以下代码执行顺序:

x=y=0
Thread1{
r1=y;
x=1;
}

Thread2{
r2=x;
y=1;
}

此时执行得到的结果则为r1=r2=0;则与我们预期结果不相符

3.以上两种问题的解决方案

添加volatile关键字,volatile关键字基本可以做到两件事:

  • 阻止编译器为了提高速度将变量缓存到寄存器而不写回
  • 阻止编译器调整操作volatile变量的指令顺序

三、CPU动态调度导致的上锁未达到预期

事实上,即使用volatile关键字阻止了编译器将变量缓存到寄存器,调整volatile变量的指令顺序,仍然有可能发生指令顺序动态调整,因为CPU在执行过程中,会对指令进行先后调整。
来看以下一段代码

volatile T* pI=0
T* GetInstance(){
	if(pI==NULL){
		lock();
		if(pI==NULL){
			pI=new T;
		}
		unlock();
	}
	return pI;
}

这里使用双重if保证了T的单例同时降低了lock()和unlock的开销,但是事实上这样的代码在CPU动态调度过程中仍然可能会发生问题。
在C++中,new其实包含了三个操作

  • 分配内存
  • 在内存的位置上调用构造函数
  • 返回内存地址

事实上,假如第一步分配内存完成后,第三步先于第二步进行,也就是先行返回了内存地址,而未调用构造函数,对象尚未构造完全的前提上,如果进行了后续对象的操作而未检验空指针,程序则会发生崩溃

解决方案

添加barrier()函数,barrier()函数保证函数之前的代码先于函数之后的代码进行。

#define barrier() _asm_ volatile ("lwsync")
volatile T* pI=0
T* GetInstance(){
	if(pI==NULL){
		lock();
		if(pI==NULL){
			T* temp =new T;
			barrier();
			pI=temp;
		}
		unlock();
	}
	return pI;
}

由于barrier的存在,对象前面的构造函数一定在pI被复制之前执行。

总结

上了锁也不一定安全。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值