在Visual Studio中,当与std::Aync一起使用时,未调用`线程_本地`变量'析构函数,这是一个错误吗?

In Visual Studio, `thread_local` variablesamp;#39; destructor not called when used with std::async, is this a bug?(在Visual Studio中,当与std::Aync一起使用时,未调用`线程_本地`变量amp;#39;析构函数,这是一个错误吗?)

本文介绍了在Visual Studio中,当与std::Aync一起使用时,未调用`线程_本地`变量'析构函数,这是一个错误吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

以下代码

#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的观察结果:

  1. 将为main()启动的三个任务分别创建一个新线程(因为它们都不会立即终止)。

  2. 这三个线程都创建一个新的线程局部变量Foo some_thread_var

    /li>
  3. 这三个任务都运行到完成,但它们正在运行的线程仍然存在(休眠)。

  4. 程序随后休眠一小段时间,然后退出,3个线程局部变量保持不变。

我运行了一些测试,除此之外,我还发现了一些关键的东西:

  • 当线程被回收时,线程局部变量被重新使用。具体地说,它们不会销毁,然后重新创建(已向您发出警告!)。
  • 如果所有异步任务都完成,并且您等待的时间足够长,则线程池将终止所有关联的线程,然后销毁线程局部变量。(毫无疑问,实际规则比这复杂得多,但这就是我观察到的)。
  • 当提交新的异步任务时,线程池会限制创建新线程的速率,希望线程在需要执行所有这些工作(创建新线程很昂贵)之前就空闲下来。因此,对std::async的调用可能需要一段时间才能返回(在我的测试中最长可达300ms)。与此同时,它只是在附近徘徊,希望它的船能进来。这种行为是有记录在案的,但我在这里把它叫出来,以防它让你大吃一惊。

结论:

  1. Microsoft的std::async实现是不一致的,但它显然是为特定目的而设计的,该目的就是很好地利用Win32线程池API。你可以因为他们公然藐视标准而痛打他们,但这种方式已经持续了很长一段时间,他们可能已经(很重要了!)依赖它的客户。我会要求他们在他们的文件中指出这一点。不这样做是犯罪。

  2. std::asyncWindows任务中使用THREAD_LOCAL变量不安全。不要这样做,它会以眼泪收场。

这篇关于在Visual Studio中,当与std::Aync一起使用时,未调用`线程_本地`变量&amp;#39;析构函数,这是一个错误吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!

本文标题为:在Visual Studio中,当与std::Aync一起使用时,未调用`线程_本地`变量&amp;#39;析构函数,这是一个错误吗?