C++ 多线程环境中的内存访问

Posted

技术标签:

【中文标题】C++ 多线程环境中的内存访问【英文标题】:Memory access in C++ multithreading environment 【发布时间】:2017-07-20 02:31:19 【问题描述】:

我有一个问题,即主线程中的函数被阻塞,直到在另一个线程中设置了局部变量。我用信号量来阻塞主线程执行:

int sendRequest(Message request, void *data)

semaphore waitForReply;
volatile int result = 0;
volatile int retData[100];
client->sendRequest(request, [&result, &retData](const Message& reply) // Callback is called from a different thread
   result = reply.result;
   memcpy(retData, reply.retData, sizeof(retData));
   waitForReply.signal();
)
waitForReply.wait();
//At this line we want result var to be updated.
memcpy(data, retData, sizeof(retData));
return result;

问题是使用 volatile int result 是否保证返回结果是从回调接收到的实际值?这是解决此问题的最佳方法还是使用普通变量和互斥锁更好?

数组 retData 的情况如何? (请不要介意数组的大小)

【问题讨论】:

不,volatile in 是不够的。 相关:What's the difference of the usage of volatile between C/C++ and C#/Java? 谢谢,你知道如何解决这个问题吗? 您的代码在我看来是正确的。 volatile 没有做任何有用的事情,你应该删除它。您的回调仅在数据被复制出来后才会发出信号量,因此您可以保证在wait() 返回后获得正确的数据。或者,换句话说,不是volatile 保证了这里的正确行为。保证来自signal() 在数据复制后被调用,wait() 将阻塞并等待该信号。 @NikosC。这是我不确定的事情,如果我错了,请纠正我。如果当result = reply.result;时,result的新值还没有写入内存而是缓存在寄存器的某个地方,那么返回的结果(return result;)是旧值怎么办? 【参考方案1】:

Volatile 在 C 或 C++ 中对于多线程代码没有用处,因为它不会创建内存屏障。但是,您的代码中没有任何内容需要内存屏障,所以您已经可以了。

错误代码的一个例子是:

// initially
my_pointer = nullptr;
ready_flag = false;

// thread 1
my_pointer = some_pointer_here;
ready_flag = true;

// thread 2
while (!ready_flag) /* wait */;
my_pointer->foo = bar;

这段代码很危险,因为很多事情都可以使ready_flag 的写入在my_pointer 的写入之前变得可见,即使它在源代码中出现在第二位。在这种情况下,您可以在分配值之前访问my_pointer

内存屏障是一条指令,它强制执行特定顺序以使内存写入可见。例如,该代码在写入my_pointer 和写入ready_flag 之间需要一个内存屏障。 C++ 标准库为此提供了std::atomic_thread_fence。此外,std::atomic 类型都可以生成内存屏障。

在您的情况下,您的变量没有相互依赖关系(除了整个操作必须已完成),而且信号量的 wait 方法几乎肯定有障碍。

在内核/CPU 之间实现缓存和内存一致性的确切方法取决于平台。 AFAIK,在 x86 和 ARM 衍生产品上,您根本不需要做任何事情。在更疯狂的平台上,这通常由内存栅栏来处理,无论如何你都会使用它。

【讨论】:

感谢您的回答,“信号量等待方法保证内存屏障”解决了我的问题。【参考方案2】:

结果缓冲区中的数据是好的。在 lambda 中对 memcpy 的调用可确保在信号量 waitForSignal 发出信号之前完整复制数据。但是……

    不,在这种情况下,volatile 关键字没有任何作用。 memcpy 不受 volatile 关键字的影响。 volatile 只影响代码的优化方式。 为什么不捕获data 并让lambda 直接填充该缓冲区?这样可以保存一份副本。 我没有看到任何说明输出缓冲区大小的参数data。这不是很好的做法。我建议您考虑使用 std::vector 或其他一些安全的方式来实现缓冲区。

【讨论】:

我希望当代码经过这一行时(//在这一行我们要更新result var。),更新data[]和result的值。如果我捕获数据并对其进行 memcpy 操作,是否会在函数返回时 data[] 的值尚未更新?如果 volatile 无法保证这一点,可以采取什么措施来实现上述预期结果?请不要介意尺寸 (3),这只是一个示例。

以上是关于C++ 多线程环境中的内存访问的主要内容,如果未能解决你的问题,请参考以下文章

多线程之美6一CAS与自旋锁

JUC多线程:JMM内存模型与volatile内存语义

C++ volatile关键字(多线程中声明为易变值不稳定值,告诉程序每次都从内存读取,不被编译优化,防止被优化后变量异常)

java中的线程安全

线程安全的了解

再次理解多线程线程安全问题(理解java内存模型后)