线程安全std::cout的死锁



我正试图通过使用Windows API的EnterCriticalSectionLeaveCriticalSection进行同步,为std::cout实现一个简单的线程安全解决方案。我认为示例代码是自我解释的,所以我们开始:

#include <cstdlib>
#include <iostream>
#include <windows.h>
namespace mynamespace {
class MyStream {
    public:
        MyStream(void) : m_lockOwner(0) {
            ::InitializeCriticalSection(&m_lock);
        }
        ~MyStream(void) {
            ::DeleteCriticalSection(&m_lock);
        }

        template <typename T>
        MyStream& operator<<(const T& x) {
            Lock();
            std::cout << x;
            return *this;
        }
        void Lock() {
            ::EnterCriticalSection(&m_lock);  // Try to get the lock, irrelevant which thread it is
            // One thread successfully received the lock and entered the critical section
            if(m_lockOwner == ::GetCurrentThreadId()) {
                // Decrease the lock count of a thread when it entered multiple times the critical section
                // e.g. mynamespace::stream << "critsec1" << "critsec2"; mynamespace::stream << mynamespace::endl;
                ::LeaveCriticalSection(&m_lock);
            }
            // Store the thread ID of the thread that holds the lock at the moment
            m_lockOwner = ::GetCurrentThreadId();
        }
        void Unlock() {
           if(m_lockOwner == GetCurrentThreadId()) {
               // Release the lock only if the calling thread is the owner
               // Note: This should be the last decrease of the lock count
               // Also reset the ownership of the lock,
               // e.g. for the case that one thread is able to enter the critical section two times in a row
               // mynamespace::stream << "crit first" << mynamespace::endl;
               // mynamespace::stream << "crit second" << mynamespace::endl;
               m_lockOwner = 0;
               ::LeaveCriticalSection(&m_lock);
           }
        }
        MyStream& operator<<(MyStream& (*endl)(MyStream&)) {
            return endl(*this);
        }
        DWORD            m_lockOwner;
        CRITICAL_SECTION m_lock;
    };
    static MyStream& endl(MyStream& stream) {
        std::cout << std::endl;
        stream.Unlock();
        return stream;
    }
    MyStream stream;
};
bool alive = true;
DWORD my_thread(LPVOID t) {
    while(alive) {
        mynamespace::stream << "OWN THREAD" << mynamespace::endl;
    }
    return 0;
}
void waitForThread(HANDLE th) {
    alive = false;
    // Wait for thread to finish
    (void)::WaitForSingleObject(th, INFINITE);
    ::CloseHandle(th);
    th = 0;
}
int main(void) {
    HANDLE th = ::CreateThread(
        NULL,
        0,
        reinterpret_cast<LPTHREAD_START_ROUTINE>(&my_thread),
        NULL,
        0,
        NULL);
    mynamespace::stream << "test print 1" << "test print 2";
    mynamespace::stream << mynamespace::endl;
    mynamespace::stream << "test print 3";
    ::Sleep(10);
    mynamespace::stream << mynamespace::endl;
    ::Sleep(10);
    waitForThread(th);
    return EXIT_SUCCESS;
}

在我的第一篇文章中,我提到在退出程序时有时会出现死锁。

这里的问题是,我没有像通过一个线程进入关键部分那样频繁地调用LeaveCriticalSection。另一个问题是我没有正确地退出子线程。

有了egurs的帮助和我的小添加,这个流现在应该是线程安全的了。

问候

您多次进入关键部分,增加其引用计数,但当您调用endl时,释放它一次,因此关键部分仍被锁定。

在类中存储线程ID,以知道该线程拥有锁。进入关键部分并比较线程ID。Equal->这不是第一个锁,因此您需要调用LeaveCriticalSection,否则将更新线程ID变量。

class MyStream {
...
     void Lock() {
         WaitForCriticalSection(&m_lock);
     }
     DWORD  m_EventOwner;
};

编辑

从原来的答案更改了我的解决方案的机制。

这是工作代码:

namespace mynamespace {
class MyStream {
    public:
        MyStream(void) {
            InitializeCriticalSection(&m_lock);
        }
        ~MyStream(void) {
            DeleteCriticalSection(&m_lock);
        }

        template <typename T>
        MyStream& operator<<(const T& x) {
            Lock();
            std::cout << x;
            return *this;
        }
        void Lock() {
            EnterCriticalSection(&m_lock);
            if (m_eventOwner == GetCurrentThreadId()) {
                LeaveCriticalSection(&m_lock);
            }
        }
        void Unlock() {
            if (m_eventOwner != GetCurrentThreadId()) {
                //error!
            }
            LeaveCriticalSection(&m_lock);
        }
        MyStream& operator<<(MyStream& (*endl)(MyStream&)) {
            return endl(*this);
        }
        DWORD  m_eventOwner;
        CRITICAL_SECTION m_lock;
    };
    static MyStream& endl(MyStream& stream) {
        std::cout << std::endl;
        stream.Unlock();
        return stream;
    }
    MyStream stream;
};

来自MSDN:

删除关键部分对象后,不要在除InitializeCriticalSectionInitializeCriticalSectionAndSpinCount之外的任何操作关键部分(如EnterCriticalSectionTryEnterCriticalSectionLeaveCriticalSection)的函数中引用该对象。如果尝试这样做,可能会发生内存损坏和其他意外错误。

如果某个关键节在其仍然拥有时被删除,则等待已删除关键节所有权的线程的状态是未定义的。

在释放关键部分之后,主线程有一个小的时间窗口,在子线程唤醒并试图锁定关键部分之前,主线程可以在这个时间窗口内安全退出。当主线程执行Sleep(100)时,它大大增加了子线程在主线程退出时锁定关键部分的可能性。

最新更新