【发布时间】: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