【发布时间】:2020-02-24 12:39:36
【问题描述】:
我正在创建一个日志对象,它在单独的std::thread 上执行真正的文件写入工作,并提供一个日志命令缓冲区接口,同步调用者线程和一个工作线程。对缓冲区的访问受到互斥锁的保护,工作线程退出条件有一个原子布尔值,并且我使用 Windows 本机事件作为信号,以在新命令到达时唤醒工作线程。对象的构造函数产生工作线程,因此它立即可用。工作线程只是一个检查退出条件的while循环,在循环中阻塞等待信号。对象的析构函数最终只是设置了退出条件,通知线程唤醒并加入它以确保在对象完全销毁之前它已关闭。
看起来很简单,当在函数的某个地方使用这样的对象时,它工作得很好。但是,当将这样的对象声明为全局变量以使其可供所有人使用时,它会停止工作。我在 Windows 上,使用带有 2015 工具链的 Visual Studio 2017。我的项目是另一个应用程序的 DLL 插件。
到目前为止我尝试过的事情:
- 在全局对象的构造函数中启动线程。然而,当我的 DLL 被加载时,这会使主线程立即挂起。在调试器中暂停应用程序表明我们在 std 库中,此时主线程应该已经启动了工作线程,现在卡在等待条件变量,大概是工作线程发出的信号推出了吗?
- 当我们第一次从其他地方使用全局对象时,按需延迟构造线程。这种方式构建它很顺利,没有挂起。但是,当通知工作线程退出析构函数时,会发送信号,但工作线程上的连接现在挂起。在调试器中暂停应用程序会发现我们的主线程是唯一还活着的线程,而工作线程已经消失了?在右大括号之前放置在工作线程函数中的断点表明它从未被命中;线程一定被杀死了?
- 我还尝试通过
std::future启动线程,以异步方式启动它,并且从全局对象中的构造函数中完美启动。然而,当未来试图加入析构函数中的线程时,它也会挂起;这里再次没有工作线程被检测到,同时没有断点被击中。
会发生什么?我无法想象这是因为线程构造和销毁发生在 main() 之外。这些 std 原语在这种时候真的应该可用,对吧?或者这是 Windows 特定的,并且代码是否在 DllMain 的 DLL_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 = %d\n", x);
// no need to make it a Meyer Singleton; we have no other global objects interfering
【问题讨论】:
-
不要重蹈覆辙,使用thread local
-
@VictorGubin:谢谢你——我不知道
thread_local是一个东西。但在我的情况下,全局对象不能使用该存储类;它将在主机应用程序加载我的插件的任何线程上创建,并且即使在创建我们自己的主机线程终止后,也应该始终可用于来自其他主机线程的未来主机调用(这可能是一件事吗?)。因此,我想要一个静态对象,它会在插件 dll 加载后立即构建并因此可用,并且在 dll 分离之前可用。 -
使用 C++ 11,您可以简单地使用经典的 Mayers Singleton,而忘记 DLLMain。 C++ 编译器/链接器将为您生成所需的正确代码。
-
哦,我认为这里的 DLLMain 和单例只是红鲱鱼。这里的问题似乎是从全局对象的构造函数启动 std::thread 似乎会带来 UB,以及从全局对象的析构函数发出信号/加入这样的线程。在这里使用 Meyers Singleton 模式确实解决了构造函数问题,可以说是在 DllMain 中将其延迟到很好(就像我手动延迟创建实际工作线程时一样),但破坏部分仍未解决......
-
我将使用我用来清理问题的代码设计更新我的帖子
标签: c++ multithreading