【问题标题】:Is this atomic pointer swap pattern safe?这种原子指针交换模式安全吗?
【发布时间】:2021-04-26 19:57:38
【问题描述】:

以下代码/模式的目标是实现高读取性能。读取发生的频率非常高,而写入(更新)的频率非常低。所以我自然会避免在读/更新中使用像互斥锁这样的排他锁。我也不想使用读取器/写入器锁定方法,因为高频率的读取操作可能会使更新线程饿死。

所以,我建议的模式是使用原子原始指针和交换。读取操作只会取消引用原子指针。更新操作将在堆上构造新数据并有一个指向它的原子指针。然后交换它们。

交换不必是原子的。我只需要避免导致指针数据损坏和崩溃的数据竞争。所以有一个时间窗口,读取线程将已经加载旧数据指针并即将取消引用它。我需要确保在所有引用旧数据指针的延迟读取线程都消失之前我不会删除旧数据指针(读取是非阻塞的)。这就是为什么我在交换后睡了一会儿。然后我删除旧的数据指针。

线程安全的实现是否存在漏洞?我认为我不能使用atomic<shared_ptr>,因为那只是在 C++20 中。所以这就像一个穷人的工作,因为atomic<shared_ptr> 无法完成。由于原子性存在线程同步,但仍然没有排他锁或读/写锁的性能影响。

std::atomic<vector<string>*> data;

// Read thread (happen with extremely high frequency):
string& Read() {
  return data->load()->at(1);
}

// Update thread (happen infrequently):
void Update() {
  std::atomic<vector<string>*> newData = new std::vector<string>();
  // insert new data to newData ...
  // ....
 
  // swap pointers (swap as whole doesn't need to be atomic, but set pointers should be atomic)
  newData.store(data.exchange(newData));

  // sleep a bit so any read thread that already has gotten the old data pointer can
  // still get the old value before we delete the old data pointer
  std::this_thread::sleep_for(100ms);

  // delete the old data (After swap, newData points to old data)
  delete newData.load();
}

【问题讨论】:

  • 我希望我们进行垃圾收集的典型案例...
  • @MarcGlisse 在 C# 中是的,我不需要进行睡眠黑客攻击然后删除。但是这种模式在需要高读取性能的情况下应该很常见(即不能使用锁)。我的主要问题是实施中有任何漏洞。我知道 Facebook 中的人使用 shared_ptr 做了类似的事情。但是他们有错误,他们认为 shared_ptr set/get 是原子的;它不是。我认为上面的代码可以在 C++20 中实现为 atomic
  • @Kenneth 并没有真正解决竞争条件,因为您仍然依赖于时间,而不是明确的“这可以使用”,“现在可以删除”之类的发信号
  • 如果你的代码依赖随机睡眠来保证正确性,那么你显然有一个错误,很可能是手头的架构问题。您可以简单地使用计数器来了解何时可以安全地删除旧数据(这是原子共享指针在您背后所做的事情)。
  • 如果性能至关重要,您可能需要查看用户空间 RCU 库,以安全地处理内存清理(睡眠不足)。如果您的应用程序寿命很短,您可能只想泄漏向量..

标签: c++ thread-safety c++17 atomic


【解决方案1】:

根据该术语的任何良好定义,您编写的代码都不“安全”。当谈到编写好的线程代码时,它不应该与出错的“概率”有关。您应该假设它会出错并使用工具确保它不会出错。

这种情况就是atomic shared_ptr accessor functions are made for。它们从 C++11 开始就存在(尽管它们在 C++20 中已被弃用,因为 atomic&lt;shared_ptr&gt; 存在)。

std::shared_ptr<vector<string>> data;

// Read thread (happen with extremely high frequency):
string& Read() {
  auto local_data = atomic_load(&data);
  //If `data` gets changed now, the destructor of `local_data` will take care of it.
  return local_data->at(1);
}

// Update thread (happen infrequently):
void Update() {
  auto newData = std::make_shared<std::vector<string>>();
  // insert new data to newData ...
  // ....
 
  // swap pointers (swap as whole doesn't need to be atomic, but set pointers should be atomic)
  newData = atomic_exchange(&data, newData);

  //Destructor takes care of deleting the old.
}

这些函数的主要警告是,当您调用其中一个函数时,如果 data 曾经是 null,它们会显示 UB。所以……不要那样做。这意味着您不能data 获得管理内存之前调用Update

因此,您需要使用对 Update 的调用之外的内容来初始化 data

这个 API 的设计是这样的,当需要切换到 C++20 的 atomic&lt;shared_ptr&gt; 时,您需要做的就是更改用于 data 的类型。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 2012-09-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-11-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多