【问题标题】:Atomic and lock-free write of arbitrary sized data/vector/array任意大小的数据/向量/数组的原子和无锁写入
【发布时间】: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


【解决方案1】:

严格来说,您的代码不是无锁的,因为您有效地使用了protector 的 LSB 来实现自旋锁。

您的解决方案看起来很像sequence lock。然而,实际的读取操作auto ret = data; 严格来说是数据竞争。公平地说,在 C++17 中编写完全符合标准的 seqlock 根本不可能,因为我们必须等待 C++20。

可以扩展 seqlock 以使读取操作无锁,但会增加内存使用量。这个想法是拥有多个数据实例(我们称它们为槽),并且写操作总是以循环方式写入下一个槽。这允许读取操作从最后一个完全写入的插槽读取。 Dmitry Vyukov 在Improved Lockfree SeqLock 中描述了他的方法。您可以查看我的seqlock implementation,它是我的xenium library 的一部分。它还可以选择允许具有可配置数量的插槽的无锁读取操作(尽管它与 Vyukov 在读取器如何找到最后一个完全写入的插槽方面略有不同)。

【讨论】:

  • 非常感谢。我不知道“序列锁”,这正是我所缺少的信息。
  • 不幸的是,要做到这一点有点棘手。如果你想自己实现一个 seqlock,我建议你看看 Hans-J 的这个演讲。勃姆:Can Seqlocks Get Along with Programming Language Memory Models?。不幸的是,我只能找到幻灯片的链接,HP 页面上随附技术报告的链接已失效。
猜你喜欢
  • 2021-07-29
  • 2016-08-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多