在Visual Studio中,当与std::Aync一起使用时,未调用`线程_本地`变量&;#39;析构函数,这是一个错误吗?
以下代码
#include <iostream>
#include <future>
#include <thread>
#include <mutex>
std::mutex m;
struct Foo {
Foo() {
std::unique_lock<std::mutex> lock{m};
std::cout <<"Foo Created in thread " <<std::this_thread::get_id() <<"
";
}
~Foo() {
std::unique_lock<std::mutex> lock{m};
std::cout <<"Foo Deleted in thread " <<std::this_thread::get_id() <<"
";
}
void proveMyExistance() {
std::unique_lock<std::mutex> lock{m};
std::cout <<"Foo this = " << this <<"
";
}
};
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::milliseconds{100});
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::milliseconds{1000});
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更新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::milliseconds{1000});
不够长,您有时可能看不到全部3条"删除"消息。
使用std::thread
而不是std::async
时,在两个平台上都会调用析构函数,并且始终会打印所有3条"Delete"消息。
解决方案
介绍性说明:我现在对此有了更多了解,因此重写了我的答案。感谢@Super、@M.M和(后来)@DavidHim和@NoSenseEtAl让我走上了正确的道路。
tl;drMicrosoft的std::async
实施不符合要求,但他们有他们的原因,如果您正确理解,他们所做的实际上可能是有用的。
对于那些不想这样做的人来说,编写一个替代std::async
的插件并不是太难,它在所有平台上都以相同的方式工作。我已经发布了一个here。
编辑:哇,最近打开的方式,我很喜欢,请参阅:https://github.com/MicrosoftDocs/cpp-docs/issues/308
让我们从头开始。cppreference有这样的话(强调和删除我的):
模板函数
async
异步运行函数f
(可能可选地在单独的线程中,该线程可能是线程池的一部分)。
然而,C++ standard表示:
如果
launch::async
设置为policy
,则[std::async
]调用[函数f]就像在新的执行线程中一样...
那么哪个是正确的呢?正如OP发现的那样,这两个语句具有非常不同的语义。当然,这个标准是正确的,就像Cang和GCC所展示的那样,那么为什么Windows的实现会有所不同呢?就像许多事情一样,它归根结底是历史。
(旧的)link that M.M dredged up除其他内容外,还有以下内容:
.微软以PPL(并行模式库)的形式实现了[
std::async
]...[而且]我可以理解这些公司急于改变规则,通过std::async
访问这些库,特别是如果它们可以显著提高性能....微软想要改变
std::async
在使用launch_policy::async.
调用时的语义,我认为在接下来的讨论中几乎排除了这一点...(基本原理如下,如果你想了解更多信息而不是阅读链接,这是非常值得的)。
而PPL基于Windows对ThreadPools的内置支持,因此@Super是对的。
那么,Windows线程池做了什么,它有什么好处呢?它旨在以高效的方式管理频繁调度、短时间运行的任务,因此第1点不要滥用它,但我的简单测试表明,如果这是您的用例,那么它可以提供显著的效率。从本质上讲,它做两件事- 它回收线程,而不必总是为您启动的每个异步任务启动新的线程。
- 它限制它使用的后台线程的总数,在此之后,对
std::async
的调用将被阻止,直到线程空闲。在我的机器上,这个号码是768。
了解了所有这些,我们现在可以解释OP的观察结果:
将为
main()
启动的三个任务分别创建一个新线程(因为它们都不会立即终止)。这三个线程都创建一个新的线程局部变量
/li>Foo some_thread_var
。这三个任务都运行到完成,但它们正在运行的线程仍然存在(休眠)。
程序随后休眠一小段时间,然后退出,3个线程局部变量保持不变。
我运行了一些测试,除此之外,我还发现了一些关键的东西:
- 当线程被回收时,线程局部变量被重新使用。具体地说,它们不会销毁,然后重新创建(已向您发出警告!)。
- 如果所有异步任务都完成,并且您等待的时间足够长,则线程池将终止所有关联的线程,然后销毁线程局部变量。(毫无疑问,实际规则比这复杂得多,但这就是我观察到的)。
- 当提交新的异步任务时,线程池会限制创建新线程的速率,希望线程在需要执行所有这些工作(创建新线程很昂贵)之前就空闲下来。因此,对
std::async
的调用可能需要一段时间才能返回(在我的测试中最长可达300ms)。与此同时,它只是在附近徘徊,希望它的船能进来。这种行为是有记录在案的,但我在这里把它叫出来,以防它让你大吃一惊。
结论:
Microsoft的
std::async
实现是不一致的,但它显然是为特定目的而设计的,该目的就是很好地利用Win32线程池API。你可以因为他们公然藐视标准而痛打他们,但这种方式已经持续了很长一段时间,他们可能已经(很重要了!)依赖它的客户。我会要求他们在他们的文件中指出这一点。不这样做是犯罪。在
std::async
Windows任务中使用THREAD_LOCAL变量不安全。不要这样做,它会以眼泪收场。
相关文章