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 是對虛假喚醒的優化,避免虛假喚醒後去爭奪鎖。

發佈了30 篇原創文章 · 獲贊 67 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章