【问题标题】:While loop not checking condition if there are no statements如果没有语句,则循环不检查条件
【发布时间】:2014-01-14 12:31:40
【问题描述】:

这应该不难理解:

bool ThreadIsRunning = false;

void Thread1(void *nothing){
    ThreadIsRunning = true;
    cout << "The thread is running!\n";
    Sleep(1000);
    cout << "Ending thread!\n");
    ThreadIsRunning = false;
    _endthread();
    return;
}


int main(){

    std::cout << "The thread is starting!\n";

    _beginthread(Thread1, 0, 0);

    std::cout << "Waiting for thread to end!\n";

    while(ThreadIsRunning);

    std::cout << "The thread is ended!\n";

    return 0;
}

所以main 线程等待Thread1ThreadIsRunning 设置为false,对吗?

是的,但事实并非如此。当它变为假时,什么都不会发生。它不应该检查永恒的价值,直到它被改变?

如果我输入while(ThreadIsRunning) Sleep(10);,它会起作用,但我认为我的代码没有必要起作用。

while(ThreadIsRunning) void(); 也不起作用。

我正在使用 Visual Studio Ultimate 2012。

C/C++ 命令行选项:

/GS /GL /analyze- /W3 /Gy /Zc:wchar_t /Zi /Gm- /O2 /sdl /Fd"Release\vc110.pdb" /fp:precise /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /errorReport:prompt /WX- /Zc:forScope /Gd /Oy- /Oi /MD /Fa"Release\" /EHsc /nologo /Fo"Release\" /Fp"Release\Test1.pch" 

链接器命令行选项:

/OUT:"<projectsfolder>\Test1\Release\Test1.exe" /MANIFEST /LTCG /NXCOMPAT /PDB:"<projectsfolder>\Test1\Release\Test1.pdb" /DYNAMICBASE "kernel32.lib" "user32.lib" "gdi32.lib" "winspool.lib" "comdlg32.lib" "advapi32.lib" "shell32.lib" "ole32.lib" "oleaut32.lib" "uuid.lib" "odbc32.lib" "odbccp32.lib" /DEBUG /MACHINE:X86 /OPT:REF /SAFESEH /INCREMENTAL:NO /PGD:"<projectsfolder>\Test1\Release\Test1.pgd" /SUBSYSTEM:CONSOLE /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /ManifestFile:"Release\Test1.exe.intermediate.manifest" /OPT:ICF /ERRORREPORT:PROMPT /NOLOGO /TLBID:1 

编辑: 没有日程/比赛/时间问题。即使我在_beginthread(Thread1, 0, 0) 之后添加Sleep(1000);,问题仍然是ThreadIsRunning 变为false 时没有任何反应

【问题讨论】:

  • 你看到“线程已结束!”在你的输出中?你的程序结束了吗?
  • 您真的在寻找这样的东西吗? stackoverflow.com/q/11779504/10077
  • 这里有竞争条件。主线程很可能会在另一个线程启动之前检查ThreadIsRunning。您还存在编译器完全优化变量访问的潜在问题,因为它没有考虑多线程。这些是在线程之间通信时应该使用同步原语(例如等待句柄)的原因。 Fred Larson 在他的评论中发布的链接是您想要的方式。
  • @HamletHakobyan 没有。程序没有结束。
  • @JimMischel 不,没有竞争条件。那不是问题。我可以添加另一个“睡眠”功能,但它仍然不会结束。是的,我也这么认为,编译器正在以一种不检查循环中变量的方式进行优化。但是如果我不想使用任何等待处理程序?为什么我不能使用我的方法?变量检查循环有什么问题?

标签: c++ multithreading while-loop


【解决方案1】:

这可以归结为线程调度 - 很有可能在线程启动之前,运行 main() 的线程到达您的 while() 语句并在线程有机会更改您的全局之前中断它布尔值true。这可能就是为什么您看到添加 Sleep 调用使线程有机会从 false 更改为 true 并且您看到了预期的行为。

【讨论】:

  • 我想过,但没有。那不是问题!还是谢谢
  • @ChristopherJanzon 好的 - 如果你有一些信息让你确信这绝对不是这种情况,那么在问题中包含这些信息以确保你得到你想要的答案是一个好主意。
  • 对不起。我认为很明显,当 'ThreadIsRunning' 设置为 false 时,问题是什么都没有发生。
【解决方案2】:

C++03 不是线程感知语言,优化器可以“清楚地看到”(不知道另一个线程可能会改变它)ThreadIsRunning 在循环体期间没有改变。一旦你添加了对sleep 的调用,编译器就必须假定 sleep 会访问ThreadIsRunning 的别名副本,并且必须在循环中的每次迭代中检查其值。

解决您的问题的方法是使用标准变量和等待循环(它将宠爱 CPU 内核)。而是在适当的时候在线程之间使用条件变量和信号,因为这是在线程之间传递此类信息的标准方式。

【讨论】:

  • 谢谢!那么我应该怎么做才能让编译器意识到ThreadIsRunning 可能会发生变化,并且它必须始终检查它的值?
【解决方案3】:

问题在于编译器可以随意将变量ThreadIsRunning 的负载提升到循环之外——在循环之前将其加载到寄存器中,然后每次都检查寄存器(而不是变量)循环。寄存器永远不会变成假,所以循环永远不会退出。

最简单和最明显的解决方法是标记变量volatile。这样,编译器就知道它无法承载负载,并且每次循环时都需要重新加载变量。

另一个使事情正常进行的“修复”是在循环中调用任何外部定义的函数(例如Sleep)。由于编译器不知道Sleep 做了什么,它必须假设它可能会更改全局变量,因此需要重新加载它。调用本地函数(之前在同一文件或头文件中定义的函数)可能不起作用,因为编译器知道该函数的作用(特别是它不会修改 ThreadIsRunning

【讨论】:

  • Volatile 不是 vanilla c++ 中的有效修复程序,因为它不提供处理器的任何可见性保证。正确的解决方法是使用 std::atomic 或某些编译器特定的方式来表示内存屏障。对于 Visual Studio,后者确实可以通过确保设置正确的命令行选项来使用 volatile 完成。
  • @Voo: _beginthread_endthread 在原版 C++ 中也不存在。 volatile 是使用这些而不是 std::thread 的旧 C++ 代码的正确解决方案,因为 std::atomic 在此类系统上不存在,并且在较新的系统上需要(除了 std::atomic),如 @987654333 @ 只是保证原子性,并不会阻止提升。
  • 如果您实际上提到了该方法的所有缺点并列出了限制,我不会投反对票:即它仅适用于 VC,并且如果您使用甚至不是默认的特定命令行标志在所有情况下。你的第二个解决方案在我所知道的任何编译器上都是完全错误的,甚至考虑了编译标志。无论如何,std::atomic 显然确实为您提供了内存排序保证,事实上您必须在加载/存储时指定它们。
  • @Voo: std::atomic 的内存排序保证不会阻止合并负载,因此不要阻止本示例中的问题。
  • 这对于顺序一致的内存排序(或获取/释放)肯定不是真的,根据gcc's summary of the models,放松模型也不是这样:“还有一个假设是从一个线程放松存储在合理的时间内被另一个线程中的宽松负载看到。这意味着在非缓存一致的架构上,宽松的操作需要刷新缓存(尽管这些刷新可以合并到几个宽松的操作中)“。
【解决方案4】:

您可能必须将 ThreadIsRunning 声明为 volatile。编译器可能对其进行了优化,因为它在 main() 中没有看到任何修改它的内容。

【讨论】:

  • 虽然volatile 确实经常被假定为它不代表的东西,但在我的测试中,它确实使这个特定的示例有效。也就是说,WaitForSingleObject(...) 是比这样的自旋循环更好的解决方案。
  • @RetiredNinja 我得到了同样的结果。 'volatile' 使它工作。但为什么?我应该用什么来代替“易失性”?为什么我要改用“WaitForSingleObject”?
  • 在这个例子中使用 volatile 并不是为了作为一种同步机制,除了防止编译器优化代码以防止检查变量的实际值。当一个线程更新另一个线程正在检查的变量时,这是使用并且通常是必要的。使用互斥量、信号量、屏障等更合适。 WaitForSingleObject 将等待线程完成,从而减少竞争条件的可能性。
  • 它依赖于平台,但volatile 通常会影响优化器处理代码的方式,以及它是否可以假设值不会因外部事件而改变。您将使用WaitForSingleObject,因为它以更有效且完全可预测的方式完成您的自旋循环所做的事情。
  • @TimDave 好的,我想我现在完全理解了!我想这是一个优化问题。为什么我不使用volatile?为什么mutexsemaphorebarrier 更合适?
【解决方案5】:

在启动线程之前添加ThreadIsRunning = true;

    std::cout << "The thread is starting!\n";

    ThreadIsRunning = true; // <============= add this

    _beginthread(Thread1, 0, 0);

    // etc.

这会告诉你这是否是一个调度问题(这很可能)。

【讨论】:

  • 谢谢,但这不是日程安排问题。
猜你喜欢
  • 1970-01-01
  • 2018-02-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-16
  • 2017-03-26
  • 2015-10-27
  • 1970-01-01
相关资源
最近更新 更多