c++11总结22——原子操作与memory_order

1. 原子操作

1.1 概念

原子操作时多线程程序中"最小且不可并行化"的操作。通常对一个共享资源的操作是原子操作的话,意味着多个线程访问该资源时,有且仅有唯一一个线程在对这个资源进行操作。

通常情况下,原子操作通过"互斥"(mutual exclusive)的访问来保证。实现互斥通常需要平台相关的特殊指令,在c++11标准之前,这常常意味着需要在c/c++代码中嵌入内联汇编代码。

1.2 存在的问题

来看一个具体的例子(例1):

#include <pthread.h>
#include <iostream>
using namespace std;

static long long total=0;
pthread_mutex_t m=PTHREAD_MUTEX_INITIALIZER;

void* func(void*)
{
     long long i;
     for(i=0;i<100000000;i++)
     {
         pthread_mutex_lock(&m);
         total+=i;
         pthread_mutex_unlock(&m);
     }
}

int main()
{
     pthread_t thread1,thread2;
     if(pthread_create(&thread1,NULL,&func,NULL))
     {
        throw;
     }

     if(pthread_create(&thread2,NULL,&func,NULL))
     {
        throw;
     }

     pthread_join(thread1,NULL);
     pthread_join(thread2,NULL);
     cout<<total<<endl;//9999999900000000
     return 0;
}

代码中为了防止数据竞争,我们使用了pthread_mutex_t的互斥锁保证两个线程可以正确的访问total。

这种方式主要存在两个问题:

1)加锁和解锁会消耗系统资源;

2)代码移植性较差,像我们实际开发过程中,一套代码中兼容windows和linux等的地方比比皆是,这其实是程序员做了"妥协"。

1.3 c++11的改进

c++11对数据进行了更加良好的抽象,引入“原子数据类型”(atomic),以达到对开发者掩盖互斥锁、临界区的目的。

来看一段代码(例2):

#include<atomic>
#include<thread>
#include<iostream>

using namespace std;
std::atomic_llong total{ 0 };//原子数据类型

void func(int)
{
    for (long long i = 0; i<100000000; ++i)
    {
        total += i;
    }
}

int main()
{
    thread t1(func, 0);
    thread t2(func, 0);
    t1.join();
    t2.join();
    cout<<total<<endl;//9999999900000000
    return 0;
}

例2的执行结果与例1相同。主要做了如下改进:通过将total定义为原子类型std::atomic_llong,使得程序不需要显示的调用API来加锁、解锁,对于代码来说,即容易又简洁。

1.4 平台相关性

c++11对常见的原子操作进行了抽象,定义了统一的接口,并根据编译选项/环境产生平台相关的实现。新标准将原子操作定义为atomic模板类的成员函数,包括了大多数典型的操作——读、写、比较、交换等。

2. atomic

2.1 定义

template <class T> struct atomic;

2.2 内置数据类型

原子类型名称对应的内置类型名称
atomic_boolbool
atomic_charchar
atomic_scharsigned char
atomic_ucharunsigned char
atomic_intint
atomic_uintunsigned int
atomic_shortshort
atomic_ushortunsigned short
atomic_longlong
atomic_ulongunsigned long
atomic_llonglong long 
atomic_ullongunsigned long long
atomic_char16_tchar16_t
atomic_char32_tchar32_t
atomic_wchar_twchar_t

2.3 atomic类型原子操作接口

可参考:

http://cplusplus.com/reference/atomic/atomic/?kw=atomic

2.4 UDT(用户自定义类型)

UDT要满足以下5个条件,才可作为模板参数去实例化atomic模板(例3):

#include <atomic>
#include <type_traits>

using namespace std;

struct MY_UDT
{
    //TODO:data member here
};

int main()
{
    auto ret = std::is_trivially_copyable<MY_UDT>::value;
    ret = std::is_copy_constructible<MY_UDT>::value;
    ret = std::is_move_constructible<MY_UDT>::value;
    ret = std::is_copy_assignable<MY_UDT>::value;
    ret = std::is_move_assignable<MY_UDT>::value;

    return 0;
}

3. 内存模型与memory_order

3.1 强顺序与弱顺序

一个具体的例子如下(例4):

#include <iostream>
#include <atomic>
#include <iostream>
using namespace std;

atomic<int> m{0};
atomic<int> n{0};

int main()
{
    int tmp = 1;
    m = tmp;
    n = 3;

    return 0;
}

伪汇编代码如下:

1: Loadi reg3, 1;    #将1放入寄存器reg3
2: Move reg4, reg3;  #将reg3的数据放入reg4 
3: Store reg4, m;    #将寄存器reg4中的数据存入内存地址m
4: Loadi reg5, 2;    #将立即数2放入寄存器reg5
5: Store reg5, n;    #将寄存器5中的数据存入内存地址n

强顺序:指令执行顺序为1->2->3->4->5

弱顺序:执行可能的执行顺序为1->4->2->5->3(指的是执行顺序存在一定的不确定性

3.2 优势与劣势

优势:提高指令执行的性能;

劣势:多线程下,可能会造成程序运行错误;

一个典型的例子(例5):

Singleton* Singleton::getInstance()
{
    if (m_instance == nullptr)
    {
        std::mutex mtx;           //函数结束时锁资源释放
        m_instance = new(std::nothrow) Singleton();
        if (m_instance == nullptr)
        {
            return nullptr;
        }
    }

    return m_instance;
}

上例为单例模式中经典的double check双检查锁的实现方式,结合上述分析,发现可能会存在如下问题:

我们默认或假定的构造顺序一般如下: 

  • 分配内存
  • 调用构造器
  • 指针返回值给instance

实际reorder后的顺序可能为:

  • 分配内存
  • 指针返回值给instance
  • 调用构造器

导致的问题:

当一个线程执行到第二步时,假如此时有另外一个线程访问,会默认m_instance不为空返回,此时实际还未调用构造器,进而导致不可预知的问题。

3.3 memory_order

3.3.1 枚举

typedef enum memory_order {
	memory_order_relaxed,
	memory_order_consume,
	memory_order_acquire,
	memory_order_release,
	memory_order_acq_rel,
	memory_order_seq_cst
	} memory_order;

定义如下:

枚举值定义规则
memory_order_relaxed不对执行的顺序作任何保证
memory_order_acquire本线程中,所有后续的读操作必须在本条原子操作完成后执行
memory_order_release本线程中,所有之前的写操作完成后才能执行本条原子操作
memory_order_acq_rel同时包含memory_order_acquire和memory_order_release
memory_order_consume在本线程中,所有后续的有关原子类型的操作,必须在本条原子操作完成之后执行
memory_order_seq_cst全部存取都按顺序执行

3.3.2 c++11对单例的修复

std::atomic<Singleton*> Singleton::m_instance;   //原子对象
std::mutex Singleton::m_mutex;

Singleton* Singleton::getInstance()
{
    Singleton* s = m_instance.load(std::memory_order_relaxed);  //屏蔽编译器的reorder
    std::_Atomic_thread_fence(std::memory_order_acquire);       //本线程中,所有后续的读操作必须在本条原子操作完成后执行
    if (s == nullptr)
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        s = m_instance.load(std::memory_order_relaxed);  //取变量
        if (s == nullptr)
        {
            s = new Singleton;   //保证不出现reorder
            std::_Atomic_thread_fence(std::memory_order_release);  //释放内存fence
            m_instance.store(s, std::memory_order_relaxed);
        }
    }

    return s;
}

3.3.3 std::atomic<T>::store

定义如下:

void atomic<T>::store( T desired, std::memory_order order = std::memory_order_seq_cst ) volatile noexcept;

memory_order参数的默认值是std::memory_order_seq_cst。实际上,atomic类型的其他原子操作接口都有memory_order这个参数,而且默认值都是std::memory_order_seq_cst

部分参考:

https://zhuanlan.zhihu.com/p/107092432

《深入理解c++11:c++11新特性解析与应用》

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值