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

Posted

技术标签:

【中文标题】可以从 Windows DLL 中的全局变量创建/销毁 std::threads 吗?【英文标题】:Can std::threads be created / destroyed from global variables in a Windows DLL? 【发布时间】:2020-02-24 12:39:36 【问题描述】:

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

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

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

在全局对象的构造函数中启动线程。然而,当我的 DLL 被加载时,这会使主线程立即挂起。在调试器中暂停应用程序表明我们在 std 库中,此时主线程应该已经启动了工作线程,现在卡在等待条件变量,大概是工作线程发出的信号推出了吗? 当我们第一次从其他地方使用全局对象时,按需延迟构造线程。这种方式构建它很顺利,没有挂起。但是,当通知工作线程退出析构函数时,会发送信号,但工作线程上的连接现在挂起。在调试器中暂停应用程序会发现我们的主线程是唯一还活着的线程,而工作线程已经消失了?在右大括号之前放置在工作线程函数中的断点表明它从未被命中;线程一定被杀死了? 我还尝试通过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 = %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 中将其延迟到很好(就像我手动延迟创建实际工作线程时一样),但破坏部分仍未解决...... 我将使用我用来清理问题的代码设计更新我的帖子 【参考方案1】:

由于您使用 C++ 编写 DLL,因此您必须了解 DLL 中的“全局变量”是如何工作的。编译器将它们的初始化粘贴在DllMain 中,然后您将在那里执行任何其他操作。但是您可以在DllMain 中执行一些严格的规则,因为它在加载程序锁定下运行。简短的总结是您不能在另一个 DLL 中调用任何东西,因为在 your DllMain 运行时无法加载该 DLL。绝对不允许调用 CreateThread,即使包裹在 std::thread::thread 构造函数中也是如此。

析构函数的问题很可能是因为您的 DLL 已退出(没有代码无法判断)。 DLL 在 EXE 之前卸载,它们各自的全局变量也按此顺序清理。任何从 EXE 中的析构函数登录的尝试都将失败,原因很明显。

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

【讨论】:

我怀疑 DllMain 是这里的罪魁祸首,但我希望(就像在 .exe 中一样)在运行任何 C++ 代码之前已经足够地设置了全局环境,我会这样就好了。因此,对于 init,我肯定需要延迟到 DLL_PROCESS_ATTACH 可以这么说(Victor 对 Meyers Singletons 的建议是一个解决方案),但破坏确实仍然是一件棘手的事情。当我向线程发出退出信号时,线程已经被操作系统杀死了?我很想只分离线程而不是 .join 破坏它......感觉已经很脏了:) @Carl : DllMain 这里还有另一个罪魁祸首,DLL_THREAD_ATTACH。如果您尝试从DllMain(DLL_PROCESS_ATTACH) 创建一个新线程,Windows 将需要在正在创建的新线程上再次调用您自己的DllMain,但由于加载程序锁定而失败。任何时候你的程序中都应该只执行一个DllMain FWIW @RaymondChen Has written about this in the past【参考方案2】:

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

可能不是最流畅的解决方案,并且您在每次访问全局变量时都有一个指针间接,但结果证明它也适合序列化全局变量的状态。

【讨论】:

以上是关于可以从 Windows DLL 中的全局变量创建/销毁 std::threads 吗?的主要内容,如果未能解决你的问题,请参考以下文章

如何从 C# 中的 C++ dll 中的全局变量从函数中获取返回数组?

易语言 程序怎样使用dll中的全局变量

Windows中的库编程

是共享库/dll中的全局变量,跨进程共享

在 C++ DLL 中使用全局变量

创建dll