在Visual Studio中,与std :: async一起使用时未调用`thread_local`变量'析构函数,这是一个错误吗?

RnM*_*Mss 22 c++ multithreading memory-leaks visual-studio

以下代码

#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() <<"\n";
    }

    ~Foo() {
        std::unique_lock<std::mutex> lock{m};
        std::cout <<"Foo Deleted in thread " <<std::this_thread::get_id() <<"\n";
    }

    void proveMyExistance() {
        std::unique_lock<std::mutex> lock{m};
        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::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;
}
Run Code Online (Sandbox Code Playgroud)

在macOS中编译并运行宽度clang:

clang++ test.cpp -std=c++14 -pthread
./a.out
Run Code Online (Sandbox Code Playgroud)

得到了结果

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
Run Code Online (Sandbox Code Playgroud)

在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
Run Code Online (Sandbox Code Playgroud)

析构函数不会被调用.

这是一个错误还是一些未定义的灰色区域?

PS

如果最后的睡眠std::this_thread::sleep_for(std::chrono::milliseconds{1000});时间不够长,有时您可能看不到所有3个"删除"消息.

当使用std::thread而不是std::async在两个平台上调用析构函数时,将始终打印所有3个"删除"消息.

Pau*_*ers 17

介绍性说明:我现在已经对此有了更多了解,因此重新编写了我的答案.感谢@super,@ MM和(后来)@DavidHaim和@NoSenseEtAl让我走上正轨.

文艺青年最爱的微软的实施std::async是不符合的,但他们有他们的理由和他们所做的实际上是有用的,一旦你正确地理解它.

对于那些不想要它的人来说,编写一个替代替代品并不是很困难,因为替换替代品std::async在所有平台上都以相同的方式工作.我在这里发了一个.

编辑:哇,这些天MS 有多开放,我喜欢它,请参阅:https://github.com/MicrosoftDocs/cpp-docs/issues/308


让我们开始吧. cppreference有这个说法(强调和删除我的):

模板函数异步async运行该函数f(可能可选地在一个单独的线程中,该线程可能是线程池的一部分).

但是,C++标准说:

如果launch::async设置为policy,[ std::async]调用[函数f] 就好像在新的执行线程中 ...

哪个是正确的?OP发现,这两个语句具有非常不同的语义.当然,标准是正确的,因为clang和gcc都显示,为什么Windows实现有所不同?就像很多事情一样,它归结为历史.

MM挖掘的(旧的)链接可以说,除其他外:

......微软std::asyncPPL(并行模式库)的形式实现[ ] ... [和]我可以理解这些公司急于改变规则并使这些库可以访问std::async,特别是如果它们可以显着改善表现......

......微软希望改变std::async调用时的语义,launch_policy::async.我认为在随后的讨论中几乎已经排除了......(理由如下,如果你想了解更多,那么阅读链接,这是值得的).

PPL基于Windows对ThreadPools的内置支持,所以@super是对的.

那么Windows线程池做了什么以及它有什么用呢?好吧,它旨在以有效的方式管理经常运行的,短期运行的任务,所以第1点不要滥用它,但我的简单测试表明,如果这是你的用例,那么它可以提供显着的效率.它本质上是两件事

  • 它会回收线程,而不必总是为您启动的每个异步任务启动一个新线程.
  • 它限制了它使用的后台线程的总数,之后调用std::async将阻塞,直到线程变为空闲.在我的机器上,这个数字是768.

所以知道这一切,我们现在可以解释OP的观察结果:

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

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

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

  4. 程序然后休眠一会儿然后退出,留下3个线程局部变量未被破坏.

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

  • 当线程被回收时,线程局部变量被重用.具体来说,它们不会被销毁然后重新创建(您已被警告过!).
  • 如果所有的asynchonous任务完成,并等待足够长的时间,线程池终止所有相关的线程和线程局部变量被销毁.(毫无疑问,实际的规则比那更复杂,但这就是我所观察到的).
  • 当提交新的异步任务时,线程池限制了创建新线程的速率,希望在需要执行所有工作之前它将变为空闲(创建新线程的代价很高).std::async因此,呼叫可能需要一段时间才能返回(在我的测试中最多300毫秒).与此同时,它只是徘徊,希望它的船将进来.这种行为有记录,但我在这里称呼它,以防它让你感到惊讶.

结论:

  1. Microsoft的实现std::async是不符合要求的,但它显然是为特定目的而设计的,其目的是充分利用Win32 ThreadPool API.你可以肆无忌惮地蔑视标准,但是这种方式已经很长时间了,他们可能有(重要的)客户依赖它.我会请他们在他们的文件中说出来.如果不这样做是犯罪.

  2. 它是不是安全使用thread_local变量std::async在Windows的任务.只是不要这样做,它会以泪水结束.