前言
前述文章讲解了标准库有系统特化的原子类,如布尔类,指针类,整数类。
但如我们所见,原子类是一个可更改对象类型的模板类,所以是可以进行泛化的,但是可转化为原子类的自定义类的条件较为苛刻。
并且复杂的自定义类原子化后,无法保证无锁结构,并且如果有自定义的赋值和比较操作,都无法完成,通常还是要用互斥进行保护,而不是将其转化为原子类。
一、自定义原子类
用自定义类型创建原子类限制因素很多:
不可含有虚函数,不可从虚基类派生,必须有编译器隐式生成拷贝赋值操作符,完全是 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 个,对象占用内存的体积不超过指针类,自定义类及其成员不含虚函数,只有平实拷贝赋值,只有逐位比较操作,可以特化为自定义原子类。
其余复杂的,还是考虑互斥保护方便。