Deadlock on static-initialized jthread calling std::stacktrace_entry::description

149 Views Asked by At

The below code results in a deadlock upon exiting main()

#include <stacktrace>
#include <iostream>
#include <thread>
#include <semaphore>
#include <chrono>

using namespace std::chrono_literals;

struct Singleton
{
    Singleton()
    {
        worker = std::jthread{ [this] {
            sema.acquire();
            for (auto& e : trace) {
                std::this_thread::sleep_for(50ms);
                std::cout << e.description() << std::endl;
            }
        } };
    }
    std::binary_semaphore sema{ 0 };
    std::stacktrace trace;
    std::jthread worker;
};

std::stacktrace g()
{
    return std::stacktrace::current();
}

std::stacktrace f()
{
    return g();
}

Singleton& get()
{
    static Singleton sing;
    return sing;
}

int main(int argc, char** argv) {
    get().trace = f();
    get().sema.release();
    std::this_thread::sleep_for(350ms);
    return 0;
}

Specifically, calling description() seems to cause a deadlock in some CRT code trying to acquire a critical section.

I hypothesize that description() calls into some CRT code which depends on a global object in the CRT managed by a critical section. Either that object is destroyed upon exiting main before the jthread destructor is called, or upon exiting main the same critical section is being entered.

If this code is somehow undefined behavior, I would be grateful for someone to point out exactly what aspect of this usage is UB.

For context, this is a minimal reproduction of a problem existing in a much larger codebase, where a logging channel object containing a worker thread and lock-free queue is being managed as a singleton.

Edit: note the sleep_for calls are purely for illustrative purposes, they are not essential nor are they an attempt to fix a race condition. The code exhibits the same deadlocking behavior if they are removed.

0

There are 0 best solutions below