【发布时间】:2021-01-16 17:06:46
【问题描述】:
我正在尝试实现以下功能:
- 任意大小的数据类型的原子和无锁写入或读取-修改-写入(在我的情况下通常是具有最多 6 个元素的浮点/整数向量)。
- 从上述数据类型进行原子读取,不会阻塞写入线程。读操作可能会被写操作阻塞。
用例:我正在尝试为 CNC 机床编写软件。电机的步进脉冲由软件在实时线程中生成。这个实时线程不断更新一个保存轴当前位置的变量。多个其他非实时线程可能会读取该变量,例如显示当前位置。
问题 1:此类问题是否有标准/公认的解决方案或模式?
我想出了以下想法:使用std::atomic<uint64_t> 来保护数据并跟踪线程当前正在写入的天气(通过检查最后一位)或自读取开始以来已写入的天气(通过在写入时增加值)。
template <class DATA, class FN>
void read_modify_write(DATA& data, std::atomic<uint64_t>& protector, FN fn)
{
auto old_protector_value = protector.load();
do
{
// wait until no other thread is writing
while(old_protector_value % 2 != 0)
old_protector_value = protector.load();
// try to acquire write privileges
} while(!protector.compare_exchange_weak(old_protector_value, old_protector_value + 1));
// write data
data = fn(data);
// unlock
protector = old_protector_value + 2;
};
template <class DATA>
DATA read(const DATA& data, std::atomic<uint64_t>& protector)
{
while(true)
{
uint64_t old_protector_value = protector.load();
// wait until no thread is writing
while(old_protector_value % 2 != 0)
old_protector_value = protector.load();
// read data
auto ret = data;
// check if data has changed in the meantime
if(old_protector_value == protector)
return ret;
}
}
问题 2:上述代码是否是线程安全的并满足上述要求?
问题3:可以改进吗?
(我能找到的唯一理论上的问题是计数器是否回绕,即在 1 次读取操作期间恰好执行了 2^63 次写入操作。如果没有更好的解决方案,我会认为这个弱点是可以接受的。)
谢谢
【问题讨论】:
-
我认为,如果您尝试在任意大小的数据类型上实现原子读/写,那么您最终将隐式或显式地使用锁。原子和无锁访问是硬件的一项功能。
-
@curiousguy 当然。它不能既是原子的又是无锁的。
-
@curiousguy 你需要一个互斥锁或其他同步设备。无需额外显式同步使用即可自动修改内存块的能力是硬件提供的一项功能。例如,x86 允许本机同步访问(通过 LOCK 前缀)仅用于通常由指令处理的数据大小,例如 1、2、4、8 字节,尽管在硬件实现级别上,我认为它在 64 字节页面中同步。不过,我很确定硬件中没有 256 字节的同步功能。
-
@curiousguy 就像 mpoeter 的回答所说的那样,OP 的代码本质上实现了一个软件自旋锁。因此不是无锁的,但它可能是原子的。此外,值得一提的是,无锁仅指软件锁,因为硬件本身可能具有某种较低级别的锁。
-
@Anonymous1847 糟糕,我认为这里存在英语问题!我将“原子访问和无锁访问”读作“原子访问以及无锁访问,(...)”,因此该短语适用于两种访问。但这意味着“既是原子的又是无锁的访问”!!!我的错!
标签: c++ multithreading atomic