std::threads可以从Windows DLL中的全局变量创建/销毁吗?

Can std::threads be created / destroyed from global variables in a Windows DLL?

本文关键字:创建 全局变量 threads DLL Windows std      更新时间:2023-10-16

我正在创建一个日志记录对象,该对象在单独的std::thread上执行真正的文件写入工作,并提供日志命令缓冲区的接口,同步调用线程和一个工作线程。 对缓冲区的访问受互斥锁保护,工作线程退出条件有一个原子布尔值,我使用 Windows 本机事件作为信号,以便在新命令到达时唤醒工作线程。 对象的构造函数生成工作线程,使其立即可用。 工作线程只是一个 while 循环检查退出条件,在循环中阻塞等待信号。 对象的析构函数最后只是设置退出条件,向线程发出唤醒信号并加入它以确保它在对象完全销毁之前关闭。

看起来很简单,当在函数中的某处使用这样的对象时,它工作得很好。 但是,当将这样的对象声明为全局变量以使其可供所有人使用时,它停止工作。 我在Windows上,使用Visual Studio 2017和2015工具链。 我的项目是另一个应用程序的 DLL 插件。

到目前为止,我尝试过的事情:

  • 在全局对象的构造函数中启动线程。 但是,当加载我的 DLL 时,这会使主线程立即挂起。 在调试器中暂停应用程序会显示我们处于 std lib 中,此时主线程应该启动工作线程,现在却在等待条件变量,大概是工作线程启动后发出的信号?
  • 当我们第一次从其他地方使用全局对象时,按需延迟构造线程。 这样构造它就可以很好地进行,而不会挂起。 但是,当向工作线程发出退出析构函数的信号时,会发送信号,但工作线程上的联接现在挂起。 在调试器中暂停应用会显示我们的主线程是唯一还活着的线程,并且工作线程已经消失了? 在工作线程函数中放置在紧靠右大括号之前的断点显示它永远不会命中;线程必须被杀死吗?
  • 我还尝试通过std::future启动线程,异步启动它,并且该线程从全局对象中的构造函数启动得非常好。 但是,当将来尝试加入析构函数中的线程时,它也会挂起;这里再次不再检测到工作线程,同时没有断点在其中命中。

这是怎么回事? 我无法想象这是因为线程构建和破坏发生在 main() 之外;这些 STD 原语应该真的在这样的时刻可用,对吧? 或者这是特定于Windows的,并且代码是否在DllMainDLL_PROCESS_ATTACH/DLL_THREAD_ATTACH事件的上下文中运行,其中启动线程可能会由于线程本地存储尚未启动和运行等而造成严重破坏?(会吗?

编辑 -- 添加的代码示例

以下是我的代码的缩写/简化;它甚至可能不会编译,但它得到了我希望:)

class LogWriter {
public:
LogWriter() :
m_mayLive(true) {
m_writerThread = std::thread(&C_LogWriter::HandleLogWrites, this); // or in initializer list above, same result
};
~LogWriter() {
m_mayLive = false;
m_doSomething.signal();
if (m_writerThread.joinable()) {
m_writerThread.join();
}
};
void AddToLog(const std::string& line) { // multithreaded client facing interface
{
Locker locker; // Locker = own RAII locker class
Lock(locker); // using a mutex here behind the scenes
m_outstandingLines.push_back(line);
}
m_doSomething.signal();
}
private:
std::list<std::string> m_outstandingLines; // buffer between worker thread and the rest of the world
std::atomic<bool> m_mayLive; // worker thread exit signal
juce::WaitableEvent m_doSomething; // signal to wake up worker thread; no std -- we're using other libs as well
std::thread m_writerThread;
int HandleLogWrites() {
do {
m_doSomething.wait(); // wait for input; no busy loop please
C_Locker locker; // access our line buffer; auto-released at end of loop iteration
Lock(locker);
while (!m_outstandingLines.empty()) {
WriteLineToLog(m_outstandingLines.front());
m_outstandingLines.pop_front();
if (!m_outstandingLines.empty()) {
locker.Unlock(); // don't hog; give caller threads some room to add lines to the buffer in between
std::this_thread::sleep_for(std::chrono::milliseconds(10));
Lock(locker);
}
};
} while (m_mayLive); // atmoic bool; no need to mutex it
WriteLineToLog("LogWriter shut down"); // doesn't show in the logs; breakpoints here also aren't being hit
return 0;
}
void WriteLineToLog(const std::string& line) {
... fopen, fprintf the line, flush, close ...
}
void Lock(C_Locker& locker) {
static LocalLock lock; // LocalLock is similar to std::mutex, though we're using other libs here
locker.Lock(&lock);
}
};

class Logger {
public:
Logger();
~Logger();
void operator() (const char* text, ...) { // behave like printf
std::string newLine;
... vsnprintf -> std::string ...
m_writer.AddToLog(newLine);
}
private:
LogWriter m_writer;
};

extern Logger g_logger; // so everyone can use g_logger("x = %dn", x);
// no need to make it a Meyer Singleton; we have no other global objects interfering

由于您正在用 C++ 编写 DLL,因此您必须了解 DLL 中的"全局变量"是如何工作的。编译器将它们的初始化粘在DllMain中,然后再执行其他任何操作。但是有一些严格的规则你可以在DllMain中执行,因为它在加载器锁定下运行。简短的摘要是,您无法在另一个 DLL 中调用任何内容,因为无法DllMain运行时加载该 DLL。绝对不允许调用CreateThread,即使包装在std::thread::thread构造函数中也是如此。

析构函数的问题很可能是因为您的 DLL 已退出(没有代码无法分辨)。DLL 在 EXE 之前卸载,并且它们各自的全局变量也按该顺序清理。由于显而易见的原因,任何从 EXE 中的析构函数进行记录的尝试都将失败。

这里没有简单的解决方案。Andrei Alexandrescu的"Modern C++ Design"为非DLL情况下的日志记录提供了合理的解决方案,但您需要对其进行强化才能在DLL中使用。另一种方法是检查日志记录函数(如果记录器仍然存在)。为此,您可以使用命名互斥锁。如果您的日志函数在OpenMutex中失败,则记录器尚不存在或不再存在。

我想我在使用 Unity 时遇到了 DLL 的破坏问题。 我当时找到的唯一解决方案是基本上放弃需要清理的真正全局变量。 相反,我将它们放在一个单独的类中,该类仅通过某个自定义启动函数实例化一次到全局指针中。然后我的DLL得到了一个"quit()"函数,也由DLL的用户调用。quit 函数会正确销毁携带全局变量的实例。

可能不是最平滑的解决方案,并且每次访问全局变量时都有一个指针间接,但事实证明,序列化全局变量的状态也很舒服。