C++11多线程-原子操作(2)

上一篇我们介绍了原子操作中最简单的std::atomic_flag,今天我们看一下std::atomic<T>类。

2. std::atomic<T>

std::atomic是一个模板类,它定义了一些atomic应该具有的通用操作,我们一起来看一下:

2.1 is_lock_free

bool is_lock_free() const noexcept;
bool is_lock_free() const volatile noexcept;

atomic是否无锁操作。如果是,则在多个线程访问该对象时不会导致线程阻塞(可能使用某种事务内存transactional memory方法实现lock-free的特性)。
事实上该函数可以做为一个静态函数。所有指定相同类型T的atomic实例的is_lock_free函数都会返回相同值。

2.2 store

void store(T desr, memory_order m = memory_order_seq_cst) noexcept;
void store(T desr, memory_order m = memory_order_seq_cst) volatile noexcept;
T operator=(T d) noexcept;
T operator=(T d) volatile noexcept;

赋值操作。operator=实际上内部调用了store,并返回d。

T operator=(T d) volatile noexpect {
    store(d);
    return d;
}

:有些编译器,在实现store时限定m只能取以下三个值:memory_order_consume,memory_order_acquire,memory_order_acq_rel。

2.3 load

T load(memory_order m = memory_order_seq_cst) const volatile noexcept;
T load(memory_order m = memory_order_seq_cst) const noexcept;
operator T() const volatile noexcept;
operator T() const noexcept;

读取,加载并返回变量的值。operator T是load的简化版,内部调用的是load(memory_order_seq_cst)形式。

2.4 exchange

T exchange(T desr, memory_order m = memory_order_seq_cst) volatile noexcept;
T exchange(T desr, memory_order m = memory_order_seq_cst) noexcept;

交换,赋值后返回变量赋值前的值。exchange也称为read-modify-write操作。

2.5 compare_exchange_weak

bool compare_exchange_weak(T& expect, T desr, memory_order s, memory_order f) volatile noexcept;
bool compare_exchange_weak(T& expect, T desr, memory_order s, memory_order f) noexcept;
bool compare_exchange_weak(T& expect, T desr, memory_order m = memory_order_seq_cst) volatile noexcept;
bool compare_exchange_weak(T& expect, T desr, memory_order m = memory_order_seq_cst) noexcept;

这就是有名的CAS(Compare And Swap: 比较并交换)。但C++11针对该操作提供了更多的细节,其操作流程如下:


以上只是个示意图,compare_exchange_weak操作是原子的,排它的。其它线程如果想要读取或修改该原子对象时,会等待先该操作完成。
该函数直接比较原子对象所封装的值与expect的物理内容,在某些情况下,对象的比较操作在使用 operator==() 判断时相等,但 compare_exchange_weak 判断时却可能失败,因为对象底层的物理内容中可能存在位对齐或其他逻辑表示相同但是物理表示不同的值(比如 true 和 5,它们在逻辑上都表示"真",但在物理上两者的表示并不相同)。
与strong版本不同,weak版允许返回伪false,即使原子对象所封装的值与expect的物理内容相同,也仍然返回false。但它在某些平台下会取得更好的性能,在某些循环算法中这种行为也是可接受的。对于非循环算法建议使用compare_exchange_strong。

2.6 compare_exchange_strong

bool compare_exchange_strong(T& expect, T desr, memory_order s, memory_order f) volatile noexcept;
bool compare_exchange_strong(T& expect, T desr, memory_order s, memory_order f) noexcept;
bool compare_exchange_strong(T& expect, T desr, memory_order m = memory_order_seq_cst) volatile noexcept;
bool compare_exchange_strong(T& expc, T desr, memory_order m = memory_order_seq_cst) noexcept;

compare_exchange的strong版本,进行compare时,与weak版一样,都是比较的物理内容。与weak版不同的是,strong版本不会返回伪false。即:原子对象所封装的值如果与expect在物理内容上相同,strong版本一定会返回true。其所付出的代价是:在某些需要循环检测的算法,或某些平台下,其性能较compare_exchange_weak要差。但对于某些不需要采用循环检测的算法而言, 通常采用compare_exchange_strong 更好。

3. std::atomic特化

我知道计算擅长处理整数以及指针,并且X86架构的CPU还提供了指令级的CAS操作。C++11为了充分发挥计算的特长,针对非浮点数值(std::atmoic<integral>)及指针(std::atomic<T*>)进行了特化,以提高原子操作的性能。特化后的atomic在通用操作的基础上,还提供了更丰富的功能。

3.1 fetch_add

// T is integral
T fetch_add(T v, memory_order m = memory_order_seq_cst) volatile noexcept;
T fetch_add(T v, memory_order m = memory_order_seq_cst) noexcept;
// T is pointer
T fetch_add(ptrdiff_t v, memory_order m = memory_order_seq_cst) volatile noexcept;
T fetch_add(ptrdiff_t v, memory_order m = memory_order_seq_cst) noexcept;

该函数将原子对象封装的值加上v,同时返回原子对象的旧值。其功能用伪代码表示为:

auto old = contained
contained += v
return old

其中contained为原子对象封装值,本文后面均使用contained代表该值。注: 以上是为了便于理解的伪代码,实际实现是原子的不可拆分的。

3.2 fetch_sub

// T is integral
T fetch_sub(T v, memory_order m = memory_order_seq_cst) volatile noexcept;
T fetch_sub(T v, memory_order m = memory_order_seq_cst) noexcept;
// T is pointer
T fetch_sub(ptrdiff_t v, memory_order m = memory_order_seq_cst) volatile noexcept;
T fetch_sub(ptrdiff_t v, memory_order m = memory_order_seq_cst) noexcept;

该函数将原子对象封装的值减去v,同时返回原子对象的旧值。其功能用伪代码表示为:

auto old = contained
contained -= v
return old

3.3 ++, --, +=, -=

不管是基于整数的特化,还是指针特化,atomic均支持这四种操作。其用法与未封装时一样,此处就不一一列举其函数原型了。

4. 独属于数值型特化的原子操作 - 位操作

4.1 fetch_and,fetch_or,fetch_xor

位操作,将contained按指定方式进行位操作,并返回contained的旧值。

integral fetch_and(integral v, memory_order m = memory_order_seq_cst) volatile noexcept;
integral fetch_and(integral v, memory_order m = memory_order_seq_cst) noexcept;
integral fetch_or(integral v, memory_order m = memory_order_seq_cst) volatile noexcept;
integral fetch_or(integral v, memory_order m = memory_order_seq_cst) noexcept;
integral fetch_xor(integral v, memory_order m = memory_order_seq_cst) volatile noexcept;
integral fetch_xor(integral v, memory_order m = memory_order_seq_cst) noexcept;

以xor为例,其操作相当于

auto old = contained
contained ^= v
return old

4.2 operator &=,operator |=,operator ^=

与相应的fetch_*操作不同的是,operator操作返回的是新值:

T operator &=(T v) volatile noexcept {return fetch_and(v) & v;}
T operator &=(T v) noexcept {return fetch_and(v) & v;}
T operator |=(T v) volatile noexcept {return fetch_or(v) | v;}
T operator |=(T v) noexcept {return fetch_or(v) | v;}
T operator ^=(T v) volatile noexcept {return fetch_xor(v) ^ v;}
T operator ^=(T v) noexcept {return fetch_xor(v) ^ v;}

5. std::atomic的限制:trivially copyable

上面我们提到std::atomic提供了通用操作,其实这些操作可以应用到所有trivially copyable的类型。trivially copyable在cppreference中文站被译为“可平凡复制”。网上也有人译作拷贝不变。一个类型如果是trivially copyable,则使用memcpy这种方式把它的数据从一个地方拷贝出来会得到相同的结果。因此本文使用拷贝不变这个中文翻译,请大家不要纠结中文翻译,明白本文所表达的意思即可。编译器如何判断一个类型是否trivially copyable呢?C++标准把trivial类型定义如下,一个拷贝不变(trivially copyable)类型是指:

  1. 没有non-trivial 的拷贝构造函数
  2. 没有non-trivial的move构造函数
  3. 没有non-trivial的赋值操作符
  4. 没有non-trivial的move赋值操作符
  5. 有一个trivial的析构函数

一个trivial class类型是指有一个trivial类型的默认构造函数,而且是拷贝不变的(trivially copyable)的class。特别注意,拷贝不变类型和trivial类型都不能有虚机制。那么trivial和non-trivial类型到底是什么呢?这里给出一个非官方、不严谨的判断方式,方便大家对trivially copyable有一个直观的认识。一个trivial copyable类在四个点上没有自定义动作,也没有编译器加入的额外动作(如虚指针初始化就属额外动作),这四个点是:

  • 缺省构造。类必须支持缺省构造,同时类的非静态成员也不能有自定义或编译器加入的额外动作,否则编译器势必会隐式插入额外动作来初始化非静态成员。
  • 拷贝构造、拷贝赋值
  • move构造、move赋值
  • 析构

为了加深理解,我们来看一下下面的例子(所有的类都是trivial的):

// 空类
struct A1 {};

// 成员变量是trivial的
struct A2 {
    int x;
};

// 基类是trivial的
struct A3 : A2 {
    // 非用户自定义的构造函数(使用编译器提供的default构造)
    A3() = default;
    int y;
};

struct A4 {
    int a;
private: // 对防问限定符没有要求,A4仍然是trivial的
    int b;
};

struct A5 {
    A1 a;
    A2 b;
    A3 c;
    A4 d;
};

struct A6 {
    A2 a[16];
};

struct A7 {
    A6 c;
    void f(); // 普通成员函数是允许的
};

struct A8 {
     int x;
    // 对静态成员无要求(std::string是non-trivial的)
     static std::string y;
};

struct A9 {
    // 非用户自定义
    A9() = default;
    // 普通构造函数是可以的(前提是我们已经有了非定义的缺省构造函数)
    A9(int x) : x(x) {};
    int x;
};

而下面这些类型都是non-trivial的

struct B {
    // 有虚函数(编译器会隐式生成缺省构造,同时会初始化虚函数指针)
    virtual f();
};

struct B2 {
    // 用户自定义缺省构造函数
    B2() : z(42) {}
    int z;
};

struct B3 {
    B3();
    int w;
};
// 虽然使用了default,但在缺省构造声明处未指定,因此被判断为non-trivial的
NonTrivial3::NonTrivial3() = default;

struct B4 {
   // 虚析构是non-trivial的
    virtual ~B4();
};

STL在其头文件<type_traits>中定义了对trivially copyable类型的检测:

template <typename T>
struct std::is_trivially_copyable;

判断类A是否trivially copyable:std::is_trivially_copyable<A>::value,该值是一个const bool类型,如果为true则是trivially copyable的,否则不是。

上一篇
C++11多线程-原子操作(1)
目录 下一篇
C++11多线程-内存模型
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • 接着上节 mutex,本节主要介绍atomic的内容,练习代码地址。本文参考http://www.cplusplu...
    jorion阅读 73,067评论 1 14
  • 接着上上节 thread ,本节主要介绍mutex的内容,练习代码地址。<mutex>:该头文件主要声明了与互斥量...
    jorion阅读 12,348评论 2 4
  • 不讲语言特性,只从工程角度出发,个人觉得C++标准委员会在C++11中对多线程库的引入是有史以来做得最人道的一件事...
    stidio阅读 13,049评论 0 11
  • “老妈,你把我扔到学校就不管我了!”电话里传来孩子的抱怨。 “妈妈是信任你啊,相信你能独立安排好自己的学习,更何况...
    阿祥妈阅读 422评论 2 9
  • 朱黎要谈一场轰轰烈烈的恋爱。 大一的学妹青春水嫩,大二的学妹买二送一,大三的学姐免费试用,大四的嘛…… 大三...
    刺客喵阅读 330评论 0 0