C++ parallel execution and thread_local object destruction

Viewed 125

I need some clarification regarding thread_local object destruction. The first question is when does it happen? C++ reference says the following:

The storage for the object is allocated when the thread begins and deallocated when the thread ends. Each thread has its own instance of the object. Only objects declared thread_local have this storage duration.

Unfortunately, it's not always obvious when a thread ends. Let's consider the following example:

#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <mutex>

struct MyClass
{
    MyClass()
    {
        std::scoped_lock<std::mutex> lck(mtx);
        std::cout << "ctor" << std::endl << std::flush;
    }

    ~MyClass()
    {
        std::scoped_lock<std::mutex> lck(mtx);
        std::cout << "dtor" << std::endl << std::flush;
    }

    static std::mutex mtx;
};

std::mutex MyClass::mtx;

int main(int argc, char* argv[])
{
    std::vector<int> v(1000000, 3);

    std::transform(std::execution::par, v.begin(), v.end(), v.begin(), [](int x)
    {
        thread_local MyClass myclass;
        return x * x;
    });

    std::cout << "done" << std::endl;
    return 0;
}

Intuitively I would expect that threads end when std::transform() call returns. However, it looks like thread_local objects of MyClass continue to live until the end of the program. Moreover, some of them don't get destroyed at all. This leads me to another question.

When I tested this sample code in MSVC 2019 (16.8.6), there were several constructors called, but always only one destructor:

ctor
ctor
ctor
ctor
ctor
done
dtor

I have read another post about this behavior in MSVC. If I understand correctly, the issue is caused by Windows system thread pool and must be Windows-specific. So I decided to test it on Linux with GCC 9.3.0:

ctor
ctor
ctor
ctor
ctor
ctor
done
dtor
dtor
dtor
dtor

Now multiple destructors are called, but still in the example above we can see an unmatched number of constructor and destructor calls. Is it a bug or a designed behavior? How can it be justified?

0 Answers
Related