多线程安全

文章讨论了内存屏障指令在确保多线程环境中的线程安全方面的重要性。由于CPU可能会乱序执行指令,可能导致数据竞争的问题,例如在双重检查锁定模式下创建单例时。文章通过示例展示了如何在ARM、x86和PowerPC架构中使用特定的barrier指令(如DMBISHST、sfence和lwsync)来防止这种乱序,以确保对象在被引用之前完全构造完成,从而维护线程安全。
摘要由CSDN通过智能技术生成

1、内存屏障指令是用来让指令变为原子指令,可以用来实现锁。

arm:

__asm__ __volatile__ ("DMB ISHST" ::: "memory");

x86:

__asm__ __volatile__ ("sfence       \n" ::: "memory");

powerpc:

__asm__ __volatile__ ("lwsync" ::: "memory");

2、举例 参考程序员的自我修养--链接、装载与库(高清带完整书签版)  1.6章节

{
if (pInst == NULL)
{
    lock();
    if (pInst == NULL)
        pInst = new T;
    unlock();
}
return pInst;
}

抛开逻辑,这样的代码乍看是没有问题的,当函数返回时,PInst总是指向一个有效的对象。而lock和unlock防止了多线程竞争导致的麻烦。双重的if在这里另有妙用,可以让lock的调用开销降低到最小。读者可以自己揣摩。

但是实际上这样的代码是有问题的。问题的来源仍然是CPU的乱序执行。C++里的new其实包含了两个步骤:

(1)分配内存。

(2)调用构造函数。

所以pInst = new T包含了三个步骤:

(1)分配内存。

(2)在内存的位置上调用构造函数。

(3)将内存的地址赋值给pInst。

在这三步中,(2)和(3)的顺序是可以颠倒的。也就是说,完全有可能出现这样的情况:pInst的值已经不是NULL,但对象仍然没有构造完毕。这时候如果出现另外一个对GetInstance的并发调用,此时第一个if内的表达式pInst==NULL为false,所以这个调用会直接返回尚未构造完全的对象的地址(pInst)以提供给用户使用。那么程序这个时候会不会崩溃就取决于这个类的设计如何了。

从上面两个例子可以看到CPU的乱序执行能力让我们对多线程的安全保障的努力变得异常困难。因此要保证线程安全,阻止CPU换序是必需的。遗憾的是,现在并不存在可移植的阻止换序的方法。通常情况下是调用CPU提供的一条指令,这条指令常常被称为barrier。一条barrier指令会阻止CPU将该指令之前的指令交换到barrier之后,反之亦然。换句话说,barrier指令的作用类似于一个拦水坝,阻止换序“穿透”这个大坝。

许多体系结构的CPU都提供barrier指令,不过它们的名称各不相同,例如POWERPC提供的其中一条指令名叫lwsync。我们可以这样来保证线程安全:

#define barrier() __asm__ volatile (”lwsync”)
volatile T* pInst = 0;
T* GetInstance()
{
    if (!pInst)
    {
        lock();
        if (!pInst)
        {
            T* temp = new T;
            barrier();
            pInst = temp;
        }
        unlock();
    }
    return pInst;
}


由于barrier的存在,对象的构造一定在barrier执行之前完成,因此当pInst被赋值时,对象总是完好的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值