30

我有一个正在运行 2 个线程的应用程序... 是否可以确定当我从一个线程更改全局变量时,另一个线程会注意到此更改?我没有任何同步或互斥系统......但这段代码是否应该一直工作(想象一个名为dataUpdated的全局布尔值):

线程 1:

while(1) {
    if (dataUpdated)
        updateScreen();
    doSomethingElse();
}

线程 2:

while(1) {
    if (doSomething())
        dataUpdated = TRUE;
}

像 gcc 这样的编译器是否会以不检查全局值的方式优化此代码,只在编译时考虑它的值(因为它永远不会同时更改)?

PS:对于类似游戏的应用程序来说,在写入值时是否会读取并不重要......重要的是其他线程会注意到更改。

4

10 回答 10

25

是的。不,也许。

首先,正如其他人所提到的,您需要使 dataUpdated 不稳定;否则编译器可以自由地将它从循环中读取出来(取决于它是否可以看到 doSomethingElse 没有触及它)。

其次,根据您的处理器和订购需求,您可能需要内存屏障。volatile 足以保证其他处理器最终会看到更改,但不足以保证更改将按照执行顺序被看到。您的示例只有一个标志,因此并没有真正显示这种现象。如果你需要并使用内存屏障,你应该不再需要 volatile

Volatile 被认为是有害的,Linux Kernel Memory Barriers是潜在问题的良好背景;我真的不知道专门为线程编写的类似内容。值得庆幸的是,线程不会像硬件外围设备那样经常提出这些问题,尽管您描述的那种情况(指示完成的标志,如果设置了标志,则其他数据假定有效)正是排序的那种事情很重要...

于 2008-09-22T23:56:36.297 回答
7

这是一个使用提升条件变量的示例:

bool _updated=false;
boost::mutex _access;
boost::condition _condition;

bool updated()
{
  return _updated;
}

void thread1()
{
  boost::mutex::scoped_lock lock(_access);
  while (true)
  {
    boost::xtime xt;
    boost::xtime_get(&xt, boost::TIME_UTC);
    // note that the second parameter to timed_wait is a predicate function that is called - not the address of a variable to check
    if (_condition.timed_wait(lock, &updated, xt))
      updateScreen();
    doSomethingElse();
  }
}

void thread2()
{
  while(true)
  {
    if (doSomething())
      _updated=true;
  }
}
于 2008-09-22T23:46:29.597 回答
7

使用锁。始终始终使用锁来访问共享数据。将变量标记为 volatile 将阻止编译器优化内存读取,但不会阻止其他问题,例如内存重新排序。如果没有锁,则无法保证 doSomething() 中的内存写入将在 updateScreen() 函数中可见。

唯一的其他安全方法是使用内存栅栏,例如显式或隐式使用 Interlocked* 函数。

于 2008-09-22T23:51:13.127 回答
6

使用volatile关键字向编译器提示该值可以随时更改。

volatile int myInteger;

以上将保证对变量的任何访问都将在没有任何特定优化的情况下进出内存,因此在同一处理器上运行的所有线程都将“看到”对变量的更改,其语义与代码读取的语义相同。

Chris Jester-Young 指出,在多处理器系统中可能会出现对这种可变值变化的一致性问题。这是一个考虑因素,它取决于平台。

实际上,相对于平台,确实有两个考虑因素需要考虑。它们是内存事务的一致性和原子性。

原子性实际上是单处理器和多处理器平台的考虑因素。出现问题是因为变量本质上可能是多字节的,问题是一个线程是否可以看到对值的部分更新。即:某些字节更改,上下文切换,中断线程读取的无效值。对于具有自然机器字长或更小的单个变量并且自然对齐的单个变量不应该是一个问题。具体来说,一个int类型在这方面应该总是可以的,只要它是对齐的——这应该是编译器的默认情况。

相对于一致性,这是多处理器系统中的一个潜在问题。问题是系统是否在处理器之间实现了完全缓存一致性。如果实施,这通常通过硬件中的 MESI 协议完成。该问题没有说明平台,但英特尔 x86 平台和 PowerPC 平台在正常映射的程序数据区域的处理器之间是缓存一致的。因此,即使有多个处理器,线程之间的普通数据内存访问也不应该担心此类问题。

与原子性相关的最后一个问题是特定于读-修改-写原子性。也就是说,您如何保证如果读取的值更新了值并写入,这会原子地发生,即使在多个处理器之间也是如此。因此,要使其在没有特定同步对象的情况下工作,将要求访问该变量的所有潜在线程仅是读取器,但期望一次只有一个线程可以成为写入器。如果不是这种情况,那么您确实需要一个可用的同步对象来确保对变量的读-修改-写操作的原子操作。

于 2008-09-23T00:08:26.000 回答
3

除其他问题外,您的解决方案将使用 100% CPU。谷歌搜索“条件变量”。

于 2008-09-22T23:25:52.563 回答
3

Chris Jester-Young 指出:

这仅适用于 Java 1.5+ 的内存模型。C++ 标准不涉及线程,并且 volatile 不保证处理器之间的内存一致性。你确实需要一个内存屏障

既然如此,唯一真正的答案是实施同步系统,对吧?

于 2008-09-22T23:36:21.913 回答
2

使用volatile关键字向编译器提示该值可以随时更改。

volatile int myInteger;
于 2008-09-22T23:24:08.977 回答
2

不,这不确定。如果您声明变量 volatile,那么编译器应该生成始终在读取时从内存中加载变量的代码。

于 2008-09-22T23:25:13.423 回答
1

如果范围是正确的(“extern”、全局等),那么就会注意到更改。问题是什么时候?并按什么顺序?

问题是编译器可以并且经常重新排序您的逻辑以填充所有并发管道作为性能优化。

它并没有真正显示在您的具体示例中,因为您的分配周围没有任何其他指令,但想象一下在您的 bool 分配之后声明的函数在分配之前执行。

在 wikipedia 上查看Pipeline Hazard 或在 google 中搜索“编译器指令重新排序”

于 2008-09-22T23:47:02.527 回答
1

正如其他人所说,volatile关键字是你的朋友。:-)

当您在 gcc 中禁用所有优化选项时,您很可能会发现您的代码可以工作。在这种情况下(我相信),它将所有内容都视为易失性,因此每次操作都会在内存中访问变量。

打开任何类型的优化后,编译器都会尝试使用寄存器中保存的本地副本。根据您的功能,这可能意味着您只能间歇性地看到变量的变化,或者在最坏的情况下,永远不会。

使用关键字volatile向编译器表明此变量的内容可以随时更改,并且不应使用本地缓存的副本。

综上所述,您可能会通过使用信号量或条件变量找到更好的结果(正如Jeff所暗示的)。

是对该主题的合理介绍。

于 2008-09-22T23:53:45.617 回答