如何确保在不同线程读取变量之前将变量存储到内存中[关闭]

Posted

技术标签:

【中文标题】如何确保在不同线程读取变量之前将变量存储到内存中[关闭]【英文标题】:How to insure variables are stored to memory before different thread reads them [closed] 【发布时间】:2012-05-22 16:59:12 【问题描述】:

更新:我以另一种形式提出了这个问题(见下文),但由于不具建设性而被关闭。有点遗憾,因为答案完全符合我的要求(并解决了我的问题),但我是新来的,所以我一定会再次尝试使其更具建设性。

我在 VC++ 中工作,在 Windows 7 下。我的多线程程序将值分配给一个线程中的变量,然后通过事件对象将信号发送到另一个被阻塞的线程,等待该信号。由于编译器提供的优化之类的东西,不能保证一个线程分配给变量的数据实际上对另一个线程可用,即使一个线程确定(通过阻塞机制)另一个线程不会尝试访问直到数据被分配给变量之后的一段时间。例如,该值可能在 CPU 寄存器中,一直保留在那里,直到需要该寄存器用于其他用途。如果在将值放入该寄存器后不久再次需要该值,这可以避免从内存中进行不必要的加载。不幸的是,这意味着内存中的相应位置继续保存它在​​分配新值之前保存的最后一个值。因此,当其他线程解除阻塞并访问保存变量值的内存时,它将获得 old 值,而不是最近分配的值。

那么,问题是:一个 Windows 线程如何强制将其分配给变量的值存储到内存中,以便另一个线程在以后确定可以访问它们?可能有几个答案,但在这个问题结束之前提供的一个似乎最适合我需要的答案是使用“内存栅栏”,这是我以前从未听说过的编程结构。遇到栅栏后,保证已完成对内存的挂起写入。 (如果栅栏是“写”栅栏;可以使用“读取”栅栏强制从内存中读取,并且可以使用“读/写”栅栏同时执行这两种操作。Windows 可以在 VC++ 中轻松使用这三个栅栏程序。)

事实证明,Windows 栅栏(又称“内存屏障”)仅将其保证应用于全局而非本地存储(原因在适用的 MSDN 页面中解释)。

如果我在这里对内存栅栏如何工作的解释不正确(并且版主曾经重新打开此问题),我很高兴看到 cmets 对此进行了解释。毕竟,我不会问我是否谦虚到承认我不知道。 (如果版主没有重新打开它,但你可以看到我有问题,请给我发电子邮件并让我知道;我很乐意帮助在我的博客上保持这个讨论的活力,如果你这样做了。)

原始版本在线程之间共享数据的好方法是什么?

我早些时候向a question 询问了volatile 变量,这为我开辟了巨大的学习经验。除其他外,我意识到我没有问对正确的问题。希望这不是不好的 *** 礼仪,但我认为我应该在这里创建一个新问题来解决我的根本问题:

我的 Visual C++ 程序中有两个线程,A 和 B。 B 被阻塞,等待来自 A 的信号。A 设置了许多变量。 A 然后向 B 发出信号,B 将读取 A 设置的变量。我担心 A 设置的某些变量实际上可能不会写回内存,因为它们可能只驻留在 CPU 寄存器中。

有什么好的方法可以确保线程 B 在读取线程 A 之前设置的变量时会读取线程 A 设置的值?

【问题讨论】:

我相信您会发现,这就是多线程的核心所在。您如何管理线程之间的共享数据? 你应该查找IPC (inter-process communication)... IPC与线程间通信无关。 @Spencer:如果您的意思是“我如何同步访问以防止出现竞争条件?”我想我已经通过让每个线程阻塞同时等待来自另一个的信号来管理它。我的特殊问题是能够保证,一旦解除阻塞,一个线程实际上可以访问另一个线程写入共享位置的值。 @Stevens :您使用的是什么阻止机制?如果是临界区或互斥体,那么您已经可以使用了,因为它们在 Windows 上具有隐式内存屏障。 【参考方案1】:

在 x86 架构下,使用好的库无需担心太多。

使用互斥锁(例如 boost::mutex)保护对共享数据的访问,如果互斥锁的实现者做对了,那么他/她将使用内存屏障(Memory Barriers @ MSDN)来确保缓存已被刷新到内存中。

如果您必须编写自己的同步代码,请为其添加内存屏障。

【讨论】:

这听起来很有希望,但我的第一次快速浏览表明这些机制用于处理原子性和重新排序。我相信我的同步方法已经解决了这些问题。 接受这个答案,因为它首先提到了内存障碍,但也感谢 Mehrdad 的回答,并为他的回答投了赞成票。 如果你使用了mutice,你已经设计错了X_X @jjechlin:作为 MUTual EXclusion 的缩写,强制转换为名词,mutex 的复数形式是 mutexesMutices 只有当这个词有一个以 -ex 结尾的直接拉丁派生词时才是正确的......而且它非常难看;-) @StevensMiller 文档有点糟糕,但 void MemoryBarrier(void); 是一个处理器命令,可确保 内存访问 不会重新排序并刷新缓存。【参考方案2】:

您在评论中提到,我的特殊问题是能够保证,一旦解除阻塞,一个线程实际上可以访问另一个线程写入共享位置的值

我相信您的问题的答案很简单:您可以使用_ReadWriteBarrier()(或者,在您的特定情况下,可能只是在阅读线程中使用_WriteBarrier)来确保您读取最新的内存价值观。

请注意,据我所知,在 C/C++ 中,volatile保证具有任何内存屏障语义——因此您不能简单地在这些语义中使用 volatile语言。内存屏障是简单读取最新值的方法。

【讨论】:

是的,是 volatile 首先让我参与了这个修复。虽然它似乎确实保证了在每次更改时将变量写入内存的一定程度的确定性(并且,不要让任何人跳过我的喉咙;我已经意识到 volatile 是一个敏感的主题 8-) ),它不能解决一般问题,即知道一个线程在一个线程解除阻塞之前设置的值是否可用于解除阻塞的线程。感谢您在内存屏障方面的领导。我会在这方面做更多的研究。 @StevensMiller:当然!另外,this thread 的解释可能比我的更好。 该线程说,“内存屏障还确保在达到屏障时执行所有挂起的读/写,因此它有效地为我们提供了我们需要的一切,使 volatile 变得不必要。我们可以删除完全是 volatile 限定词。”这听起来像是一个修复,如果通过“挂起的读/写”来表示从/到内存的读/写。是这个意思吗?我将深入研究 MSDN,看看是否可以确认。再次感谢。 @StevensMiller:是的,就是这个意思——寄存器不受影响。 这似乎奏效了!我已经放弃了对volatile 的所有使用,并在发出阻塞线程信号之前简单地添加了_WriteBarrier()。现在,您提到在 reading 线程中添加_WriteBarrier()。该线程没有挂起的写入。它还会在那里工作吗?无论我把它放在哪个线程中,它都会起作用吗?感谢所有的帮助。我今天学到了很多!【参考方案3】:

这就像问“编写面向对象程序的好方法是什么”。除了这个问题,我会说“去读一本好书”,但对于这个问题,真的没有一本关于坏范式的好书。许多多线程编程是基于最小化共享数据的使用而不是很好地使用它。

所以,我的建议是:

1) 设计为没有两个线程需要以这种方式相互通信。听起来更像是一个程序线程,而不是两个真正独立的线程。

2) 在您的流程内或流程之间实施面向服务的架构。使所有共享数据发生在短暂的请求/响应模式上,而不是依赖于使用轮询的全局变量。 A 设置并告诉 B 读取的所有这些变量听起来很像“客户端”A 发送给“服务器”B 的“请求”。

3) 如果您可以安装并努力学习库,我推荐 ZMQ。我在这方面有很好的经验,他们宣传(并根据我的经验提供)他们的工具,该工具在服务上看起来像一个用于实现客户端和服务器的库,作为摆脱线程之间所有共享数据的一种方式。如果没有别的,文档可能会为您提供很好的方法来考虑在线程之间兑现您的共享数据以获取不涉及它们的模式。

【讨论】:

我明白了,但我试图具体说明两个同步线程以及确保缓存在寄存器中的值在解除阻塞等待线程之前写入内存的特定问题。我认为“编写面向对象程序的好方法是什么?” WRT #1:你是对的,但我正在与之通信的线程是由 Windows API 调用创建的。我无法从我的程序中设计出来。 #2:我的全局变量没有被轮询;我将线程与每个线程同步,交替等待另一个线程告诉它解除阻塞。 #3:我喜欢一个好的库,但我无法重新设计我正在使用的系统的结构。 您正在描述请求/回复模式。共享数据是一个方向的请求和另一个方向的回复。 “deblock”命令是在您发送请求时,“block”正在侦听请求。我就是这样实现它的,这样所有共享数据的生命周期和范围都是有限的。 这听起来很适合我正在做的事情。有限的生命周期和范围没问题,afaik。为我提供了有关如何以保证对共享数据的读/写都来自/到相同位置的方式实现模式的参考或指针(也就是说,这将避免我的寄存器缓存问题m 寻址)? 如果我理解正确 - 简单的方法是全局 RequestFromA* a_req; B 将访问以从 A 中查找要在请求中使用的数据的变量。问题是,这仅在 A 和 B 中的一个恰好存在时才有效。但是,您想要的模式是“中介者模式”(请参阅​​设计模式或谷歌。)【参考方案4】:

如果您的意思是“如何同步访问以防止出现竞争条件?”我想我已经通过在等待来自另一个线程的信号时让每个线程阻塞来管理它。我的特殊问题是能够保证,一旦解除阻塞,一个线程实际上可以访问另一个线程写入共享位置的值。

是的,没错。问题是等待某个线程设置的信号不足以确保从当前线程可以看到该线程的任何其他活动。一个线程可以设置一个变量,触发信号,然后等待信号的线程可以访问该变量,但是得到一个完全不同的值。

我目前很喜欢 Anthony Williams' 的书,C++ Concurrency in Action,关于这个主题。答案似乎在于正确使用 std::atomic 内存顺序。这是一个例子:

std::atomic<bool> signal(false);
std::atomic<int> i(0);

-- thread 1 --
i.store(100,std::memory_order_relaxed);
signal.store(true,std::memory_order_release);

-- thread 2 --
while(!signal.load(std::memory_order_acquire));
assert(i.load(std::memory_order_relaxed) == 100);

当第二个线程看到信号时,在使用 memory_order_release 执行的存储和使用 memory_order_acquire 执行的加载之间建立关系,这保证了对 i 的存储将在第二个线程中可见。因此断言保证成立。

另一方面,如果您使用不太严格的内存顺序,那么您将得不到任何保证。

-- thread 1 --
i.store(100,std::memory_order_relaxed);
signal.store(true,std::memory_order_relaxed);

-- thread 2 --
while(!signal.load(std::memory_order_relaxed));
int i2 = i.load(memory_order_relaxed);
// No guarantees about the value loaded from i!

或者,您可以只使用默认内存顺序,只要您没有任何数据竞争,就可以保证顺序一致性。

std::atomic<bool> signal(false);
int i = 0;

-- thread 1 --
i = 100;
signal = true;

-- thread 2 --
while(!signal);
assert(i == 100);

【讨论】:

以上是关于如何确保在不同线程读取变量之前将变量存储到内存中[关闭]的主要内容,如果未能解决你的问题,请参考以下文章

volatile关键字详解

深入理解volatile关键字

java 关键字volatile

Java内存模型学习笔记

多线程——volatile

Java内存模型与线程