2022-09-05 C++并发编程(十九)

C++并发编程(十九)


前言

前述文章讲解了标准库有系统特化的原子类,如布尔类,指针类,整数类。

但如我们所见,原子类是一个可更改对象类型的模板类,所以是可以进行泛化的,但是可转化为原子类的自定义类的条件较为苛刻。

并且复杂的自定义类原子化后,无法保证无锁结构,并且如果有自定义的赋值和比较操作,都无法完成,通常还是要用互斥进行保护,而不是将其转化为原子类。


一、自定义原子类

用自定义类型创建原子类限制因素很多:

不可含有虚函数,不可从虚基类派生,必须有编译器隐式生成拷贝赋值操作符,完全是 memcpy() ,

若有基类或非静态数据成员,则它们也要服从上面条件。

同时,比较替换操作是 memcmp() 操作,完全是逐位比较。所以对于需 “内存对齐” 的自定义类,需要小心,compare_exchange_strong() 函数的可能结果会出人意料,也会出现 “伪失败”。

另外如果自定义类的成员变量所占的总字节数大于 int 或 指针类,编译器可能比较难编译为原子指令,也就是无法构成无锁结构。

所以对于复杂的自定义类,还是要考虑基于互斥的保护结构。

以下是示例代码,由 double 特化的原子类,和自定义类特化的原子类,分别演示其使用。

虽然 double 和 float 是内置类型,但很遗憾,没有原子运算,和自定义类一样,只有最基本的原子操作:初始化,判定是否无锁(较复杂自定义类无法编译成功此函数),原子读写,原子交换,原子比较替换。

#include <atomic>
#include <iostream>

struct atomicObj
{
    atomicObj() = default;

    atomicObj(int I, char F)
        : i(I)
        , f(F)
    {}

  private:
    int i = 0;
    char f = 0;
};

auto main() -> int
{
    //初始化double原子类
    std::atomic<double> atomicDouble(1.0);

    //判断是否无锁结构
    std::cout << atomicDouble.is_lock_free() << std::endl;

    //原子存储,可配合写操作内存顺序
    atomicDouble.store(2.0);

    //原子读取,可配合读操作内存顺序
    double showDouble = atomicDouble.load();

    //原子交换,可配合读改写内存操作顺序
    showDouble = atomicDouble.exchange(3.0);

    //原子比较替换,可配合读改写内存操作顺序
    //如果是自定义类型,其比较是通过 memcmp() 进行的,而非我们定义的 == 重载符号
    //同时,如果自定义类型因对齐等原因含有填充位(padding),则比较替换操作在两值相等时,也会失败
    while (!atomicDouble.compare_exchange_weak(showDouble, 4.0))
    {
    }

    showDouble = 4.00;

    //原子比较替换,可配合读改写内存操作顺序
    atomicDouble.compare_exchange_strong(showDouble, 5.0);

    //用自定义类型创建原子类限制因素很多
    //不可含有虚函数,不可从虚基类派生,必须有编译器隐式生成拷贝赋值操作符
    //若有基类或非静态数据成员,则它们也要服从上面条件
    atomicObj test(1, 'a');

    //初始化还是一样
    std::atomic<atomicObj> atomicObjTest(test);

    //判定是否无锁函数,
    //在我的编译环境,自定义类型的元素成员大于3,或虽只有两项但含有double类,
    //则无法编译通过
    std::cout << atomicObjTest.is_lock_free() << std::endl;

    atomicObj test2(2, 'b');

    //原子存储,可配合写操作内存顺序
    atomicObjTest.store(test2);

    //原子读取,可配合读操作内存顺序
    test = atomicObjTest.load();

    test2 = atomicObj(3, 'c');

    //原子交换,可配合读改写内存操作顺序
    test = atomicObjTest.exchange(test2);

    test = atomicObj(3, 'c');

    test2 = atomicObj(4, 'd');

    //原子比较替换,可配合读改写内存操作顺序
    //如果是自定义类型,其比较是通过 memcmp() 进行的,而非我们定义的 == 重载符号
    //同时,如果自定义类型因对齐等原因含有填充位(padding),则比较替换操作在两值相等时,也会失败
    while (!atomicObjTest.compare_exchange_weak(test, test2))
    {
    }

    test = atomicObj(3, 'c');

    test2 = atomicObj(4, 'd');

    //原子比较替换,可配合读改写内存操作顺序
    //如果是自定义类型,其比较是通过 memcmp() 进行的,而非我们定义的 == 重载符号
    //同时,如果自定义类型因对齐等原因含有填充位(padding),则比较替换操作在两值相等时,也会失败
    //如果strong版本不在循环中,结果不可预料
    //在我的编译环境中,以下值虽相等,但依然返回 false
    bool result = atomicObjTest.compare_exchange_strong(test2, test);

    //第二次返回 true,原因是自定义类含有两个成员变量,
    // int: Type: int Offset: 0 bytes Size: 4 bytes 和
    // char: Type: char Offset: 4 bytes Size: 1 byte (+3 padding)
    // char 后有填充随机3字节,导致比较操作时 memcmp() 结果为 false
    result = atomicObjTest.compare_exchange_strong(test2, test);

    return 0;
}

总结

对于简单的自定义类,比如成员变量不超过 3 个,对象占用内存的体积不超过指针类,自定义类及其成员不含虚函数,只有平实拷贝赋值,只有逐位比较操作,可以特化为自定义原子类。

其余复杂的,还是考虑互斥保护方便。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不停感叹的老林_<C 语言编程核心突破>

不打赏的人, 看完也学不会.

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值