thread_local + std::thread deadlock on destruction
Asked Answered
O

1

17

Anyone knows what kind of UB is this? The following code deadlocks on jthread destruction when built with MSVC 19.29.30148, sometimes it deadlocks after std::cout and sometimes before. This is somehow related to thread_local, but I cannot see what is the issue. It seems to work fine under other compilers and platforms.

#include <thread>
#include <memory>
#include <iostream>

int main(void)
{
    std::thread t2(
        [&] {
            thread_local std::jthread asd([] {
                    std::cout << "ASD" << std::endl;
                    });
        });
    if (t2.joinable()) { t2.join(); }
}

Update: Reproducible both on static and dynamic runtime

Ozell answered 18/9, 2023 at 10:31 Comment(3)
Looks like a C++ library bug to me.Tamarah
@SamVarshavchik, yep, to me too. It seems the issue is in "join" operation called during thread_local destruction (even through many layers of indirection). It is somehow causes issues in MS runtime, but I am not sure if they mentioned/documented it anywhere.Ozell
There is reported bug: thread_local Type Containing std::thread Never Finishes Execution. Looks the same issue.Fourth
P
-2

Its difficult to describe this as a library bug.

What is happening.

thread_local std::thread asd

This is in a thread, creates a thread and stores it in thread local storage.

When thread t2 completes, it starts the thread destruction process. This process is naturally single threaded - 2 threads can't end at the same time, and they get serialized.

The threaded mechanisms note that there is thread_local storage for that thread, and start calling the destructor for asd, which includes invoking the join function.

So we wait for asd to be able to complete. Unfortunately it is unable to do this, as the thread destruction of asd is blocked by the thread t2 - which is in progress. Hence the deadlock.

Fixes.

  1. Store item asd in normal memory, not thread local.
  2. join asd before the end of t2's function.
  3. Ensure that asd has run before getting to the end of t2.

The library writers have a really difficult set of goals to achieve. They need to ensure that thread-local data is tidied up in a timely manner. If thread local destruction is delayed until after the thread is finished, referred to data which went out of scope with the thread, there would be bugs.

If they (as implemented), tidied up in the context where the thread is in the process of being destroyed, they face this form of inter-thread deadlock.

Update

Running on linux (wasn't sure what Clang and GCC meant apart from linux), I confirmed that the program as is,

  1. Ran the constructed asd thread before the t2 thread completed
  2. Delaying execution of the asd thread showed the destruction of thread_local asd didn't slow or stop the destruction of thread-local-storage data (__GI__call_tls_dtors)

So I would say that the linux implementation has issues where a thread stored in thread_local may refer to other thread_local data which may be destroyed.

The use of thread_local for asd is completely unnecessary, and is the cause of the problem

Petroglyph answered 22/7 at 6:31 Comment(2)
I don’t understand why asd needs to wait for t2 here. The program seems to work fine in Clang and GCC, so clearly this isn’t a hard requirement?Lycaon
So it's a bug in the C++ spec itself?Chukar

© 2022 - 2024 — McMap. All rights reserved.