这种原子指针交换模式安全吗?

问题描述

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

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

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

线程安全的实现有什么漏洞吗?我不能使用 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();
}

解决方法

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

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

atomic<shared_ptr>

这些函数的主要警告是,当您调用其中一个函数时,如果 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,它们会显示 UB。所以……不要那样做。这意味着您不能调用 null,直到 Update 获得管理的内存。

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

此 API 的设计是,当需要切换到 C++20 的 Update 时,您只需更改用于 atomic<shared_ptr> 的类型。