【发布时间】:2015-09-15 07:49:14
【问题描述】:
背景 -- 我们开发C++11 代码并使用 gtest/gmock 编写单元测试。这是在 Windows 服务器上使用 SCons 和 MinGW 中的 g++ 构建的。在执行单元测试时,我们开始偶尔遇到问题:静默退出、预期错误、异常弹出……没有明显的模式或共性,也不容易重现。最终,一位同事将其范围缩小到一个明显的线程加入的情况,甚至没有开始执行其有效负载功能。在这种情况下,没有例外或类似情况。由于未达到预期,测试完全失败。然后我做了一个更简单的测试用例,既不涉及我们的代码库也不涉及 gtest/gmock。
BRIEF QUESTION -- 考虑以下代码 sn-p:
bool flag(false);
std::thread worker( [&] () { flag = true; } );
worker.join();
assert(flag);
当执行一次时,这似乎工作正常。 “一次”是指在测试可执行文件中一次。然后这个可执行文件从一个命令文件中重复运行多次。
但是,当在测试本身中重复执行时,上述断言通常会失败;有时在第二次重复时,有时在数千次重复后。
似乎 g++ std::thread 在 MinGW (4.8.0/32) 下表现不佳——线程已成功创建(即没有异常),它是可连接的,并且可以连接。但是,在某些情况下,它的有效载荷功能从未执行过。 -- 我知道 MinGW 没有完整的 POSIX pthreads,我已经看过 Using threads with MinGW?、pthread_create not enough space、MinGW and std::thread 等,但无济于事。我们确实使用静态链接(出于不同的原因),我还找到了https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57740。
这一切都指向线程实现中的竞争条件。在测试中同时设置布尔标志 volatile 并关闭优化 (-O0) 没有任何区别。
我们目前在 32 位 MinGW 版本 4.8.0 中使用 g++(从 QT5.1 安装开箱即用),现在正在考虑迁移到一个不同的工具链(例如 Linux 机器上的 gcc/g++),或者至少升级到更高版本的 MinGW,如果有迹象表明这个问题可能已得到修复。
这是 MinGW 上 std::thread 的已知问题吗? 是否有任何修复或解决方法?(我的意思是一般修复。我已经实施了一些似乎可行但我不喜欢的解决方案。)
FULL DETAILS -- 执行下面的代码我们注意到:
[A] 运行下面的代码(到目前为止)执行永远不会在 #2 测试失败。 [预期]
[B] 但是,在 #4 的测试经常失败(在不同次数的重复之后,包括仅两次(!)重复;虽然有时需要数千测试失败)。 [意外]
[C] 仅在 #1 启用等待会导致 #4 出现 更多 条件失败( #2 的测试仍然没有失败)。 [意外]
[D] 仅在 #3 处启用等待使 #2 和 #4 处的测试都成功. [嗯...]
使用 [D] “修复”并在 数千次trong> 重复之后,我已经看到(到目前为止两次)害怕 R6016(-没有足够的空间用于线程数据)。 (在某种程度上,这是可以理解的,也许并不那么担心,只要在测试之间定期恢复线程资源并且测试不是背靠背运行。)
请注意,#1 和 #3 的“等待”只是为了说明 - 它们没有超时并且可能会挂起。
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <thread>
int main(int, char *[])
{
bool flag1(false);
assert(not flag1);
std::thread worker1( [&] () { flag1 = true; } );
assert(worker1.joinable());
// while (not flag1) { std::this_thread::yield(); } // #1: MAKES #4 FAIL MORE OFTEN
worker1.join();
if (not flag1) // #2: DOES NOT FAIL
{
puts("Oops on first!");
exit(EXIT_FAILURE);
}
bool flag2(false);
assert(not flag2);
std::thread worker2( [&] () { flag2 = true; } );
assert(worker2.joinable());
// while (not flag2) { std::this_thread::yield(); } // #3: MAKES #4 SUCCEED
worker2.join();
if (not flag2) // #4: SOMETIMES FAILS
{
puts("Oops on second!");
exit(EXIT_FAILURE);
}
puts("Both OKAY");
return EXIT_SUCCESS;
}
编译成test.exe,上面的测试可以重复使用:
@ECHO OFF
FOR /L %%i IN (1,1,1000000) DO (
ECHO __ %%i ________________________________________________________________________________ %%i __
test.exe
IF ERRORLEVEL 1 GOTO gameover
)
:gameover
编辑
- 正如@TC 指出的那样,使用 bool 是不正确的。最初,我使用
atomic_bool,其行为与上述相同。然后我错误地将示例“简化”为 bool。 - 顺便说一句,仅使用
yield而不检查 #1 和 #3 的标志是不足够的。
【问题讨论】:
-
在线程创建或线程连接中似乎缺少内存屏障或优化屏障。
-
看来这是一个错误。使用您提供的代码,我在 MinGW 4.8.0 上得到完全相同的结果。我也用在线编译器(IdeOne 和melpon.org/wandbox)试过这个,没有任何问题
-
我有逐个案例的解决方法,但不是一般修复(目前在应用程序方面很好)。想法?
-
旁注:#1 和 #3 都会引发数据竞争,因此行为未定义。
-
你是对的。我起初使用 atomic_bool,但后来(错误地)简化为简单的 bool。顺便说一句,我与原子版本的行为完全相同。
标签: multithreading c++11 mingw stdthread