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

Viewed 719

I'm creating a logging object which performs the real file writing work on a separate std::thread, and offers an interface to a log command buffer, syncing the caller threads and the one worker thread. Access to the buffer is protected by a mutex, there's an atomic bool for the worker thread exit condition, and I'm using Windows native Events as a signal to wake up the worker thread when new commands arrive. The object's constructor spawns the worker thread so it is immediately available. The worker thread is simply a while loop checking the exit condition, with in the loop a blocking wait for the signal. The object's destructor finally just sets the exit condition, signals the thread to wake up and joins it to ensure it's down before the object is fully destroyed.

Seems simple enough, and when using such an object somewhere in a function it works nicely. However, when declaring such an object as a global variable to have it usable for everyone it stops working. I'm on Windows, using Visual Studio 2017 with the 2015 tool chain. My project is a DLL plugin for another application.

The things I tried so far:

  • Start the thread in the constructor of the global object. This however makes the main thread hang immediately when my DLL is loaded. Pausing the app in the debugger reveals we're in the std lib, at a point where the main thread should have launched the worker thread and is now stuck waiting for a condition variable, presumably one that is signaled by the worker thread once it is launched?
  • Delay-construct the thread on demand when we first use the global object from somewhere else. This way constructing it goes nicely without a hang. However, when signalling the worker thread to exit from the destructor, the signal is sent, but the join on the worker thread now hangs. Pausing the app in the debugger reveals our main thread is the only one still alive, and the worker thread is already gone? A breakpoint placed in the worker thread function right before the close brace reveals it is never hit; the thread must be getting killed?
  • I also tried to start the thread via a std::future, starting it up async, and that one launches perfectly fine from the constructor in global objects. However, when the future tries to join the thread in the destructor, it hangs as well; here again no worker thread to be detected anymore while no breakpoint gets hit in it.

What could be going on? I can't imagine it's because the thread construction and destruction takes place outside main() so to speak; these std primitives should really be available at such moments, right? Or is this Windows specific and is the code running in the context of DllMain's DLL_PROCESS_ATTACH / DLL_THREAD_ATTACH events, where starting up threads might wreak havoc due to thread local storage not yet being up and running or such? (would it?)

EDIT -- added code sample

The following is an abbreviation/simplification of my code; it probably doesn't even compile but it gets the point across I hope :)

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
2 Answers

Since you're writing a DLL in C++, you have to understand how "globals" in DLL's work. The compiler sticks their initialization in DllMain, before anything else that you would do there. But there are some strict rules what you can do in DllMain, as it runs under loader lock. The short summary is that you can't call anything in another DLL because that DLL cannot be loaded while your DllMain is running. Calling CreateThread is definitely not allowed, not even if wrapped inside a std::thread::thread constructor.

The problem with the destructor is quite possibly because your DLL has exited (can't tell without code). The DLL unloads before the EXE, and their respective globals are also cleaned up in that order. Any attempt to log from a destructor in an EXE will fail for obvious reasons.

There is no simple solution here. Andrei Alexandrescu's "Modern C++ Design" has a reasonable solution for logging in the non-DLL case, but you'll need to harden that for use in a DLL. An alternative is to check in your logging functions if your logger still exists. You can use a named mutex for that. If your log function fails in OpenMutex, then either the logger does not exist yet or it no longer exists.

Think I've encountered that destruction issue with DLLs to use with Unity. The only solution I found back then was to essentially give up true global variables that would need cleanup. Instead I put them in a separate class which is instantiated only a single time into a global pointer by some custom launch function. Then my DLL got a "quit()" function also called by the user of the DLL. The quit function correctly destroys the instance carrying the global variables.

Probably not the smoothest solution and you have a pointer-indirection on every access to the global variables, but it turned out to be comfortable for serializing the state of the global variables as well.

Related