对 std::atomic::load 的结果使用结构取消引用 (->) 运算符是否安全

Is it safe to use the Structure dereference(->) operator on the result of std::atomic::load

本文关键字:gt 安全 是否 运算符 引用 取消 load atomic std 结果 结构      更新时间:2023-10-16

在尝试使用std原子指针时,我遇到了以下内容。假设我这样做:

std::atomic<std::string*> myString;
// <do fancy stuff with the string... also on other threads>
//A can I do this?
myString.load()->size()
//B can I do this?
char myFifthChar = *(myString.load()->c_str() + 5);
//C can I do this?
char myCharArray[255];
strcpy(myCharArray, myString.load()->c_str());

我很确定C是非法的,因为在此期间myString可能会被删除。

但是我不确定 A 和 B。我想它们是非法的,因为在执行读取操作时指针可能会被尊重。

但是,如果是这种情况,您怎么能从可能被删除的原子指针中读取。由于加载是 1 步,数据读取是 1 步。

// A can I do this?
myString.load()->size()

是的,您可以,但如果其他内容可能正在变异或破坏/解除分配您收到myString快照指向的string,则您确实存在竞争条件。 换句话说,以原子方式检索指针后的情况与多个线程可能具有指针的任何std::string对象的情况相同,只是......

有一个问题是,原子load是否保证对string的某些特定构造/更改 - 可能由更新myString以指向您load指向的特定string实例的任何线程执行 - 将对您可见。 默认设置是确保这一点,但您可能需要阅读此memory_order参数的说明以load()。 请注意,显式要求内存同步并不能防止其他线程发生突变/破坏。

所以,假设myString()依次指向stringab然后c,你的代码检索到&b......只要stringb在你打电话时没有变异或破坏/解除分配size(),你就没问题。myString()可能会更新为指向c之前/期间/之后,这并不重要,因为您拨打b.size()

退一步说,程序可能很难知道在你调用load()后多久你可能会尝试取消引用指针,如果b对象稍后要发生突变或破坏/解除分配,你提出的调用类型不会在以后的突变/破坏的任何同步中合作。 显然,您可以通过多种方式添加此类协调(例如,其他一些原子计数器/标志,使用条件变量通知潜在的修饰符/析构函数/删除器......),或者您可能决定有时接受这样的竞争条件(例如,如果已知b是大尺寸 LRU 缓存中的最新条目之一)。

如果您正在做一些事情,例如在多个static const string实例周围循环myString,则不必担心上面的所有突变/破坏内容(好吧,除非您在main()之前/之后访问它们)。

// B can I do this?
char myFifthChar = *(myString.load()->c_str() + 5);

是的,有上面的所有警告。

// C can I do this?
char myCharArray[255];
strcpy(myCharArray, myString.load()->c_str());

是的,如上所述(并且受提供的缓冲区足够大的限制)。

我很确定C是非法的,因为在此期间myString可能会被删除。

如上所述 - 这种担忧对您提到的所有 3 种用途同样有效,只是对于 C 来说可能性更大,因为复制需要更多的 CPU 周期才能完成,而不是恢复垃圾值,输掉比赛可能会导致缓冲区溢出。

我很确定C是非法的,因为myString可能会在 同时。

您的所有示例也是如此。由于原子负载,唯一安全的是负载本身 - 仅此而已。您有责任确保对加载的内容进行任何后续操作的安全性。在这种情况下,没有,所以它非常不安全。

从原子指针加载的唯一方法是确保您拥有结果 - 像std::shared_ptr<T>一样,或者保证它存活更长的生命周期,并且您应该禁止所有写入。

有人提到你的方法是有风险的。以下是您可能需要考虑的事项:使用具有不可变值的std::shared_ptr<const std::string>,以及shared_ptratomic_load和atomic_store。std::shared_ptr将确保您不会访问悬空指针,而不变性(字符串在构造后不会更改)将保证对字符串本身的访问是线程安全的,因为标准定义的所有const方法都是线程安全的。

编辑:按照要求解释我所说的"风险业务"的含义:如果您使用std::atomic<std::string *>,那么很容易意外引入竞争条件,例如

// Data
std::atomic<std::string *> str(new std::string("foo"));
// Thread 1
std::cout << *str.load();
// Thread 2
*str.load() = "bar"; // race condition with read access in thread 1
// Thread 2 (another attempt using immutable instances)
auto newStr = new std::string("bar");
auto oldStr = str.exchange(newStr);
delete oldStr;  /* race condition with read access in thread 1
because thread 1 may have performed load() before
the exchange became visible to it, and may not
be finished using the old object. */

请注意,这与operator <<无关,即使只是在线程 1 中的字符串上调用size()也会导致竞争条件。

在实践中,人们可能会看到"修复",例如使用不可变字符串在更新中的delete之前添加sleep,以便线程 1 有足够的时间使用旧指针完成其业务。尽管这可能在特定实现中大部分时间都有效,但它不会引入真正的排序(在标准语中是发生之前的关系C++因此不是正确的 rsp。便携式解决方案。

如果另一个线程可能会修改或删除string对象,那么所有这些都是非法的。

使用atomic

会同步对指针的访问,但你不会同步对指针指向的对象的访问。