在 Visual Studio 中,与 std::async 一起使用时未调用“thread_local”变量的析构函数,这是一个错误吗?
Posted
技术标签:
【中文标题】在 Visual Studio 中,与 std::async 一起使用时未调用“thread_local”变量的析构函数,这是一个错误吗?【英文标题】:In Visual Studio, `thread_local` variables' destructor not called when used with std::async, is this a bug? 【发布时间】:2018-11-26 14:37:05 【问题描述】:以下代码
#include <iostream>
#include <future>
#include <thread>
#include <mutex>
std::mutex m;
struct Foo
Foo()
std::unique_lock<std::mutex> lockm;
std::cout <<"Foo Created in thread " <<std::this_thread::get_id() <<"\n";
~Foo()
std::unique_lock<std::mutex> lockm;
std::cout <<"Foo Deleted in thread " <<std::this_thread::get_id() <<"\n";
void proveMyExistance()
std::unique_lock<std::mutex> lockm;
std::cout <<"Foo this = " << this <<"\n";
;
int threadFunc()
static thread_local Foo some_thread_var;
// Prove the variable initialized
some_thread_var.proveMyExistance();
// The thread runs for some time
std::this_thread::sleep_for(std::chrono::milliseconds100);
return 1;
int main()
auto a1 = std::async(std::launch::async, threadFunc);
auto a2 = std::async(std::launch::async, threadFunc);
auto a3 = std::async(std::launch::async, threadFunc);
a1.wait();
a2.wait();
a3.wait();
std::this_thread::sleep_for(std::chrono::milliseconds1000);
return 0;
在 macOS 中编译和运行宽度 clang:
clang++ test.cpp -std=c++14 -pthread
./a.out
得到结果
Foo Created in thread 0x70000d9f2000 Foo Created in thread 0x70000daf8000 Foo Created in thread 0x70000da75000 Foo this = 0x7fd871d00000 Foo this = 0x7fd871c02af0 Foo this = 0x7fd871e00000 Foo Deleted in thread 0x70000daf8000 Foo Deleted in thread 0x70000da75000 Foo Deleted in thread 0x70000d9f2000
在 Visual Studio 2015 Update 3 中编译并运行:
Foo Created in thread 7180 Foo this = 00000223B3344120 Foo Created in thread 8712 Foo this = 00000223B3346750 Foo Created in thread 11220 Foo this = 00000223B3347E60
不调用析构函数。
这是一个错误还是一些未定义的灰色区域?
附言
如果最后的睡眠 std::this_thread::sleep_for(std::chrono::milliseconds1000);
不够长,有时您可能看不到所有 3 条“删除”消息。
当使用std::thread
而不是std::async
时,在两个平台上都会调用析构函数,并且始终会打印所有 3 个“删除”消息。
【问题讨论】:
IIRC MSVC 为std::async
使用线程池。这可能意味着在未来可用后,运行该函数的线程仍然存在。 (这里只是推测)
The issue is discussed here
@M.M 是的,谢谢,我昨天看到了,但没有时间正确阅读。我觉得现在可能有点过时了。
@PaulSanders 在我看来,那里提出的问题还没有得到解决(正如这个问题所证明的那样)
@M.M 是的,我同意,这是一个复杂的问题,标准并没有真正正确地解决它(它做出了太多不适合线程池的相当无聊的承诺)。我计划在今天晚些时候更详细地研究 MSVC 的实现,并将我的发现发回给我的答案。我认为 Linux 完全通过包装 std::thread
来回避这个问题——也许 MS 在这里尝试了一点too。
【参考方案1】:
介绍性说明:我现在已经对此了解了很多,因此重新编写了我的答案。感谢@super、@M.M 和(后来)@DavidHaim 和 @NoSenseEtAl 让我走上了正确的道路。
tl;dr Microsoft 对 std::async
的实现不符合标准,但它们有其原因,并且一旦您正确理解,它们所做的事情实际上是有用的。
对于那些不希望这样的人,编写一个直接替换 std::async
的替换代码并不难,它在所有平台上都以相同的方式工作。我已经发了一个here。
编辑:哇,MS 这些天开放,我喜欢它,请参阅:https://github.com/MicrosoftDocs/cpp-docs/issues/308
让我们从头开始。 cppreference 有这个说法(强调和删除我的):
模板函数
async
异步运行函数f
(可能可选地在单独的线程中可能是线程池的一部分)。
但是,C++ standard 是这样说的:
如果
launch::async
设置在policy
中,[std::async
] 调用[函数 f] 就像在新的执行线程中一样 ...
那么哪个是正确的?正如 OP 所发现的,这两个语句具有非常不同的语义。好吧,标准当然是正确的,正如 clang 和 gcc 所示,那么为什么 Windows 实现会有所不同呢?就像很多事情一样,它归结为历史。
(老)link that M.M dredged up 有这样的说法,除其他外:
...微软以PPL(并行模式库)的形式实现了[
std::async
]...[并且]我可以理解那些公司急于打破规则并制作这些库可通过std::async
访问,尤其是如果它们可以显着提高性能......微软希望在使用
launch_policy::async.
调用时更改std::async
的语义我认为这在随后的讨论中几乎被排除...(原理如下,如果您想了解更多信息,请阅读链接,非常值得)。
而且 PPL 是基于 Windows 对ThreadPools 的内置支持,所以@super 是对的。
那么 Windows 线程池有什么作用,它有什么用呢?嗯,它旨在以一种有效的方式管理频繁调度的、短期运行的任务,所以第 1 点是不要滥用它,但我的简单测试表明,如果这是你的用例,那么它可以提供显着的效率。本质上,它做了两件事
它会回收线程,而不必总是为您启动的每个异步任务启动一个新线程。 它限制了它使用的后台线程总数,之后对std::async
的调用将阻塞,直到线程空闲。在我的机器上,这个数字是 768。
知道了这一切,我们现在可以解释 OP 的观察结果了:
为main()
启动的三个任务中的每一个创建一个新线程(因为它们都不会立即终止)。
这三个线程中的每一个都创建一个新的线程局部变量Foo some_thread_var
。
这三个任务都运行完成,但它们运行的线程仍然存在(休眠)。
然后程序会休眠一小会儿然后退出,保留 3 个线程局部变量未破坏。
我进行了许多测试,除此之外,我还发现了一些关键的东西:
当一个线程被回收时,线程局部变量被重新使用。具体来说,它们不会被销毁然后重新创建(已警告您!)。 如果所有异步任务都完成并且您等待的时间足够长,那么线程池将终止所有关联的线程,然后销毁线程局部变量。 (毫无疑问,实际规则比这更复杂,但这是我观察到的)。 在提交新的异步任务时,线程池会限制创建新线程的速率,希望在需要执行所有工作(创建新线程太贵了)。因此,对std::async
的调用可能需要一段时间才能返回(在我的测试中最多需要 300 毫秒)。与此同时,它只是四处游荡,希望它的船能进来。这种行为已记录在案,但我在这里指出它以防让你感到意外。
结论:
Microsoft 对std::async
的实现不符合标准,但它显然是为特定目的而设计的,该目的是为了充分利用 Win32 ThreadPool API。你可以因为他们公然藐视标准而痛打他们,但这种做法已经存在很长时间了,他们可能有(重要的!)依赖它的客户。我会要求他们在他们的文档中指出这一点。不这样做是犯罪。
在 Windows 上的 std::async
任务中使用 thread_local 变量是不安全的。千万别做,会以眼泪收场。
【讨论】:
就我而言,我不能用局部变量替换thread_local
。问题中的代码只是重现该问题的示例。嗯......现在,这个问题,如果线程在进程退出之前从未被破坏,那么 thread_local 变量永远不会破坏......没有析构函数,它就不再是 C++ 了。只要我瞄准 Windows,我可能不得不将std::async
扔进最深的深渊。 :( 好伤心。
@Rnmss 使用线程池进行异步在大多数其他语言中非常常见,因为它对于通常的场景来说是如此明显的胜利(由于相关成本高,Linux 下的异步无法在许多情况下使用) .你不应该依赖其他不做同样事情的实现——原因很简单,现在 Linux 下不存在基础设施。有解决方案,从显式传递状态到显式处理生命周期。
@RnMss 问题是,你不能同时拥有它。 要么,您接受并使用std::async
的语义来获得[我们希望] 它带来的效率或您使用std::thread
来确保您的@987654350 @ 对象在您希望它们消失时消失。由于我给出的原因,thread_local
绝对不打算与std::async
一起使用。如果你这样做,那么你就坐在定时炸弹上——在任何平台上。我认为这值得一票,就个人而言,至少你现在知道自己的立场了。
@voo 我看到 Linux 有 一些 类型的线程池支持,请参阅:linux.die.net/man/3/cp_thread_pool。好用吗,你知道吗?我看到手册页相当腼腆地简单地说“线程池实现”。可能意味着任何事情。
@Paul 我第一次听说它。上次我用 gcc 检查std::async
至少没有使用它,这使得std::async
的整个想法变得毫无意义——如果你没有线程池支持,std::async
基本上只是std::thread
和一些附加的功能(这也意味着我无法使用它)。【参考方案2】:
看起来只是 VC++ 中的许多错误中的另一个。 考虑一下 n4750 的这句话
所有使用 thread_local 关键字声明的变量都有线程 储存期限。这些实体的存储将持续到 创建它们的线程的持续时间。有一个与众不同的 每个线程的对象或引用,并且使用声明的名称是指 与当前线程关联的实体。 2 一个变量 线程存储持续时间应在其第一次使用 odr 之前初始化 (6.2) 并且,如果构造,应在线程退出时销毁。
+这个
如果实现选择 launch::async 策略,- (5.3) 调用 到异步返回对象上的等待函数,该对象共享 此异步调用创建的共享状态应阻塞,直到 关联线程已完成,好像已加入,否则超时 (33.3.2.5);
我可能是错的(“线程退出”与“线程完成”,但我觉得这意味着 thread_local 变量需要在 .wait() 调用解除阻塞之前被销毁。
【讨论】:
一旦你读到,我想你会明白,将其描述为“VC++ 中的许多 bug 中的另一个”是过于简单化并且有点侮辱 MS,除了有点“旋转”之外,是making a big effort 成为符合标准的(这不是一项小任务)。问题是,std::async
的当前定义实际上有点混乱,所以像我们这样的人误入歧途也就不足为奇了。
我知道那个链接。除了公然的公关谎言之外,我没有从中学到任何我不知道的东西。 MSVC 在标准一致性方面仍落后于 clang 和 gcc。
我到处打听,学到了很多东西。我更新了我的答案,以防你感兴趣。是的,基本上你是对的,但正如我的回答所解释的那样,有一些缓解因素。以上是关于在 Visual Studio 中,与 std::async 一起使用时未调用“thread_local”变量的析构函数,这是一个错误吗?的主要内容,如果未能解决你的问题,请参考以下文章
当 Visual Studio 2017 与 std 标头不兼容时,如何使用最高警告级别(墙)?
在 Visual Studio 中,与 std::async 一起使用时未调用“thread_local”变量的析构函数,这是一个错误吗?
在 Visual Studio 中调用 std::swap 时的 std::bad_function_call