C++多线程-内存同步

5

我有两个函数,它们被单独的线程调用:

void SetPtr(T* ptr_)
{
   ptr = ptr_;
}
void Process()
{
   if(ptr != nullptr)
   {
      ptr->fun(); // fun() can call Process() again
   }
}

我知道在调用Process之前必须先调用SetPtr,但因为这些函数由不同的线程调用,内存可能没有同步,即Process可能会看到初始的ptr值,或尝试读取未完全写入的ptr。我可以使用std::lock_guard来提供同步,但我想看看其他可能性。有哪些其他方法可以为这种情况提供内存同步? 编辑1SetPtr可以随时被调用时,如何同步内存(无需互斥量)? 编辑2 ptr, SetPtr和Process属于同一个类,ptr最初设置为nullptr

你如何在没有互斥锁的情况下进行任何同步? - ForceBru
你如何知道在有人调用Process之前是否已经调用了SetPtr?这非常重要,请参考ComicSansMS的答案,该答案假定您拥有坚不可摧、可移植的保证。 - MSalters
3个回答

3
我知道在调用Process之前会调用SetPtr。 你已经完成了。 为确保调用SetPtr发生在调用Process之前所需的同步机制也将确保Process观察到的指针是由SetPtr设置的那个指针。 标准术语的推理如下:线程#1中对SetPtr的调用在线程#2中对Process的调用之前发生了“跨线程发生之前”。这由程序中的某个同步点P来保证。全局ptr的写入在线程#1中的P之前被顺序化(因为它们在同一线程上发生)。此外,P在线程#2中全局ptr的读取之前被顺序化。请注意,编译器和硬件不允许重新排列它们中的任何一个穿过P。关键属性在于“顺序之前”的组合与“跨线程发生之前”的组合。 因此,在线程#1上写入ptr是在线程#2上读取之前发生的。没有数据竞争。

但是在哪里可以看到ptr是全局的? - David Haim
@ComicSansMS,我已经一年前与其他人辩论过这个问题。你不能保证这一点。例如,调度可能完全在Posix模型之外完成——比如通过对“poll”和“eventfd”的调用。虽然可以期望这些调用实际上会施加内存屏障,但它们并没有得到任何保证。 - SergeyA
1
@SergeyA,这是有道理的。但是,如果你开始混合这样的线程模型,那么你最好知道你在做什么。要建立一个适用于线程间同步的局面,但不适用于有序操作的可传递性,是极其困难的。在纯标准C++中,这是完全不可能做到的。由于没有迹象表明OP正在做一些疯狂的事情,所以我假定不存在这样的权术构造。当然,在现实世界的C++中,人们总是需要考虑核心语言之外的系统特定影响。 - ComicSansMS
1
@SergeyA 也可以说,OP的声明:“我知道在某人调用Process之前会调用SetPtr”是以C++标准为基础定义的,因此必须使用标准保证的原语。否则它还能表示什么? - Arvid
@Arvid,这是根据其他对OP可用的同步来定义的吗? - SergeyA
显示剩余4条评论

1
您可以使用 std::atomic<T*>
std::atomic<T*> ptr;
void SetPtr(T* ptr_)
{
   ptr.store(ptr_, std::memory_order_release);
}
void Process()
{
   auto _ptr = ptr.load(std::memory_order_acquire);
   if(_ptr != nullptr)
   {
      _ptr->fun(); // fun() can call Process() again
   }
}

正如您所说,SetPtrProcess 之前被调用,因此这种设计可以使用这种方法。如果不能保证在 Process 之前调用 SetPtr,则 Process 可能会遭受 ABA 问题 的影响。

据我所知,全局的 std::atomic<T*> ptr; 不像普通的 T *ptr; 一样被初始化为 nullptr,所以也许应该将其改为 std::atomic<T*> ptr{nullptr}; 以使逻辑正常工作。 - nwp
请考虑编辑1。 - Irbis
请注意,您的Process函数尝试无谓地两次加载ptr。另外,ptr->fun()无法编译。 - Maxim Egorushkin
而且,ptr 最初具有不确定的值,这会导致 Process 失败。 - Maxim Egorushkin
1
@MSalters 全局变量会被初始化为0,因此全局的T*ptr将被初始化为nullptr。然而,全局的std::atomic<T*> ptr;则不会被初始化为nullptr - nwp
显示剩余8条评论

1
如果您需要在线程之间共享标量类型(整数、指针、枚举)的变量,应使用松散的原子操作:
std::atomic<T*> ptr{nullptr}; // Do not forget to assign an initial value.

void SetPtr(T* ptr_) {
    ptr.store(ptr_, std::memory_order_relaxed);
}

void Process() {
    if(auto ptr_ = ptr.load(std::memory_order_relaxed)) // Load it once.
        ptr_->fun();
}

放松的原子操作是可用的最便宜的原子操作。

@DavidHaim 刚刚阅读了文档 http://en.cppreference.com/w/cpp/atomic/memory_order: 对于任何特定的原子变量,所有修改都按照特定于该原子变量的总顺序发生... 标记为 memory_order_relaxed 的原子操作不是同步操作,它们不会排序内存。 它们只保证原子性和修改顺序的一致性。 - Maxim Egorushkin
你自己的引用说明为什么松散原子在这里不适用 - 它们不能对内存进行排序。 - SergeyA
@SergeyA 我只能建议阅读该文档:std::memory_order 指定如何在原子操作周围排序常规的非原子内存访问。 这里没有其他变量参与。 - Maxim Egorushkin
我只能向你提出同样的建议。 - SergeyA
@SergeyA 你有什么实质性的话要说吗? - Maxim Egorushkin
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接