下面的代码会导致退出时死锁
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;
}
具体来说,调用
description()
似乎会在某些尝试获取关键部分的 CRT 代码中导致死锁。
我假设
description()
调用了一些 CRT 代码,这些代码依赖于 CRT 中由关键部分管理的全局对象。该对象要么在调用 jthread
析构函数之前退出 main 时被销毁,要么在退出 main 时进入相同的临界区。
如果此代码在某种程度上是未定义的行为,我将不胜感激有人准确指出此用法的哪些方面是 UB。
对于上下文来说,这是更大的代码库中存在的问题的最小再现,其中包含工作线程和无锁队列的日志记录通道对象被作为单例进行管理。
编辑:请注意
sleep_for
调用纯粹是为了说明目的,它们不是必需的,也不是试图修复竞争条件。如果删除它们,代码会表现出相同的死锁行为。
我认为这个错误是由于
atexit
函数(包括本地静态对象析构函数)在同一锁下调用,这些函数被枚举。这最终会陷入僵局。
CRT 中的永久修复是避免从锁内调用用户代码。要么通过在调用用户代码之前解锁,要么使用无锁列表。但不幸的是,这会对性能产生严重影响。也许这就是为什么这个问题还没有解决的原因。
建议搜索/报告到https://developercommunity.visualstudio.com/看看是否可以修复。
作为解决方法,我建议在构造单例之前使用
stacktrace
机械一次。然后它将更早初始化,因此稍后销毁。请注意,current
是轻量级的,不需要全局对象,您必须使用一些字符串转换函数。
struct Singleton
{
Singleton()
{
// Instantiate stacktrace machinery to prevent deadlock
std::to_string(std::stacktrace::current());
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;
};