C++11/14介紹(七)——語言級線程支持(二)

std::mutex, std::unique_lock

C++11引入了 mutex 相關的類,其所有相關的函數都放在 <mutex> 頭文件中。

std::mutex 是 C++11 中最基本的 mutex 類,通過實例化 std::mutex 可以創建互斥量,而通過其成員函數 lock() 可以僅此能上鎖,unlock() 可以進行解鎖。但是在在實際編寫代碼的過程中,最好不去直接調用成員函數,因爲調用成員函數就需要在每個臨界區的出口處調用 unlock(),當然,還包括異常。這時候 C++11 還爲互斥量提供了一個 RAII 語法的模板類std::lock_gurad。RAII 在不失代碼簡潔性的同時,很好的保證了代碼的異常安全性。

在 RAII 用法下,對於臨界區的互斥量的創建只需要在作用域的開始部分,例如:

void some_operation(const std::string &message) {
    static std::mutex mutex;
    std::lock_guard<std::mutex> lock(mutex);

    // ...操作

    // 當離開這個作用域的時候,互斥鎖會被析構,同時unlock互斥鎖
    // 因此這個函數內部的可以認爲是臨界區
}

由於 C++保證了所有棧對象在聲明週期結束時會被銷燬,所以這樣的代碼也是異常安全的。無論 some_operation() 正常返回、還是在中途拋出異常,都會引發堆棧回退,也就自動調用了 unlock()。

而 std::unique_lock 則相對於 std::lock_guard 出現的,std::unique_lock 更加靈活,std::unique_lock 的對象會以獨佔所有權(沒有其他的 unique_lock 對象同時擁有某個 mutex 對象的所有權)的方式管理 mutex 對象上的上鎖和解鎖的操作。所以在併發編程中,推薦使用 std::unique_lock。例如:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void block_area() {
    std::unique_lock<std::mutex> lock(mtx);
    //...臨界區
}
int main() {
    std::thread thd1(block_area);

    thd1.join();

    return 0;
}

std::future, std::packaged_task

std::future 則是提供了一個訪問異步操作結果的途徑,這句話很不好理解。爲了理解這個特性,我們需要先理解一下在 C++11之前的多線程行爲。

試想,如果我們的主線程 A 希望新開闢一個線程 B 去執行某個我們預期的任務,並返回我一個結果。而這時候,線程 A 可能正在忙其他的事情,無暇顧及 B 的結果,所以我們會很自然的希望能夠在某個特定的時間獲得線程 B 的結果。

在 C++11 的 std::future 被引入之前,通常的做法是:創建一個線程A,在線程A裏啓動任務 B,當準備完畢後發送一個事件,並將結果保存在全局變量中。而主函數線程 A 里正在做其他的事情,當需要結果的時候,調用一個線程等待函數來獲得執行的結果。

而 C++11 提供的 std::future 簡化了這個流程,可以用來獲取異步任務的結果。自然地,我們很容易能夠想象到把它作爲一種簡單的線程同步手段。

此外,std::packaged_task 可以用來封裝任何可以調用的目標,從而用於實現異步的調用。例如:

#include <iostream>
#include <future>
#include <thread>

int main() {
    // 將一個返回值爲7的 lambda 表達式封裝到 task 中
    // std::packaged_task 的模板參數爲要封裝函數的類型
    std::packaged_task<int()> task([](){return 7;});
    // 獲得 task 的 future
    std::future<int> result = task.get_future();    // 在一個線程中執行 task
    std::thread(std::move(task)).detach();    std::cout << "Waiting...";
    result.wait();
    // 輸出執行結果
    std::cout << "Done!" << std:: endl << "Result is " << result.get() << '\n';
}

在封裝好要調用的目標後,可以使用 get_future() 來獲得一個 std::future 對象,以便之後事實線程同步。
std::condition_variable

std::condition_variable 是爲了解決死鎖而生的。當互斥操作不夠用而引入的。比如,線程可能需要等待某個條件爲真才能繼續執行,而一個忙等待循環中可能會導致所有其他線程都無法進入臨界區使得條件爲真時,就會發生死鎖。所以,condition_variable 實例被創建出現主要就是用於喚醒等待線程從而避免死鎖。std::condition_variable的 notify_one() 用於喚醒一個線程;notify_all() 則是通知所有線程。下面是一個生產者和消費者模型的例子:

#include <condition_variable>
#include <mutex>
#include <thread>
#include <iostream>
#include <queue>
#include <chrono>

int main()
{
    // 生產者數量
    std::queue<int> produced_nums;
    // 互斥鎖
    std::mutex m;
    // 條件變量
    std::condition_variable cond_var;
    // 結束標誌
    bool done = false;
    // 通知標誌
    bool notified = false;

    // 生產者線程
    std::thread producer([&]() {
        for (int i = 0; i < 5; ++i) {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            // 創建互斥鎖
            std::unique_lock<std::mutex> lock(m);
            std::cout << "producing " << i << '\n';
            produced_nums.push(i);
            notified = true;
            // 通知一個線程
            cond_var.notify_one();
        }   
        done = true;
        cond_var.notify_one();
    }); 

    // 消費者線程
    std::thread consumer([&]() {
        std::unique_lock<std::mutex> lock(m);
        while (!done) {
            while (!notified) {  // 循環避免虛假喚醒
                cond_var.wait(lock);
            }   
            while (!produced_nums.empty()) {
                std::cout << "consuming " << produced_nums.front() << '\n';
                produced_nums.pop();
            }   
            notified = false;
        }   
    }); 

    producer.join();
    consumer.join();
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章