c++11 之 std::condition_variable


比較常見的一個使用 std::condition_variable 場合就是線程池的消息隊列。邏輯線程(可能多個)將消息推入消息隊列,線程池中的工作線程(多個)會從消息隊列中取出消息進行處理,如果隊列中沒有消息則進入睡眠狀態等待消息。

本文將通過這種消息隊列的實現,來分析如何使用 std::condition_variable 以及使用過程中的注意事項。

先看下這個消息隊列的最終實現:

void Push(void *msg)
{
    std::unique_lock<std::mutex> lock(m_mutex);
    m_queue.push(msg);
    lock.unlock();
    m_cond.notify_one();

    return;
}

void * WaitAndPop()
{
    void *msg = nullptr;

    while (true)
    {
        std::unique_lock<std::mutex> lock(m_mutex);
        if (!m_queue.empty())
        {
            msg = m_queue.front();
            m_queue.pop();
            return msg;
        }

        while(m_queue.empty()) m_cond.wait(lock);
    }

    // return nullptr;
}

為什么需要搭配一個互斥量使用?

先假設不需要搭配互斥量使用,代碼如下

// WaitAndPop
mutex.lock();
if (!queue.empty)
{
    // pop msg
    ...
}
mutex.unlock();
// 標注
cond.wait();

queue 會被不同線程使用,所以需要一個鎖來同步。
這個鎖必須在 cond.wait 前解鎖,否則工作線程進入睡眠狀態導致邏輯線程的 Push 無法獲得鎖。
那么問題來了,當 WaitAndPop 執行到 mutex.unlock 后 cond.wait 前時,邏輯線程執行了 Push ,意味着 cond.notify_one 在 cond.wait 前執行了。結果就是 工作線程進入睡眠,但是消息隊列中還有一個消息沒被處理 。如果后續沒有新消息,那這個消息就只能永遠呆在隊列中了。
std::condition_variable::wait 需要一個鎖作參數基本上避免了這種情況,但是不排除有的同學將這個鎖和用來同步queue操作的鎖分開來而導致這種情況。


Push 中調用 lock.unlock 和 cond.notify_one 的順序問題

這是個性能優化的問題,誰先誰后對結果並沒有影響。

  • unlock 在前,notify_one 在后。
    工作線程在被喚醒前,邏輯線程已經解鎖,這使得工作線程在喚醒后就能直接獲得鎖進入處理流程。

  • notify_one 在前,unlock 在后。
    工作線程在被喚醒后,邏輯線程可能還沒有解鎖,這將導致工作線程無法獲得鎖而又進入睡眠狀態等待鎖。這里多了一次上下文切換,會損失一定性能。


虛假喚醒

虛假喚醒的意思是即使沒有調用 cond.notify_one , cond.wait 也有可能返回。
留意下面這段代碼:

// WaitAndPop
std::unique_lock<std::mutex> lock(m_mutex);
if (!m_queue.empty())  // 位置1
{
    ...
}

while(m_queue.empty()) m_cond.wait(lock); // 位置2

位置1 就是對虛假喚醒的判斷處理,這一步一定要做,而且還要在獲得鎖后做。

位置2 是對虛假喚醒的優化,避免虛假喚醒后去爭奪鎖。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM