C++多線程編程



author: lunar
date: Tue 13 Oct 2020 06:43:01 PM CST

C++11 多線程開發

C++11之前,C++對於並發編程並沒有提供語言級別的支持。只能像C語言那樣使用操作系統提供的POSIX提供的多線程庫pthread。

C++11之后,C++通過一系列語法支持使得多線程開發變得更容易。

溫馨提示:如果你是在命令行通過g++編譯,需要添加-lpthread參數鏈接pthread庫。否則會報"undefined reference pthread_create"的錯。

線程

首先復習一下操作系統的部分知識:

  • 一個進程內的多個線程共享進程的地址空間、內存資源。
  • 每個線程所單獨擁有的僅為:線程狀態、寄存器、程序計數器、堆棧。

線程的創建

C++11中線程的創建非常簡單,就是調用std::thread的構造函數。

#include <thread>

void func() {
    //do something
}

int main() {
    std::thread t(func);
}

線程對象創建即啟動,並需要像java那樣通過一個start函數進行啟動。

如果func是一個有參函數的話,func后面還可以添加func的參數。

當然,構造函數的參數並不只能是函數,任何可調用對象包括lambda表達式都可以作為參數構造線程。

注意:這樣創建的線程最大的缺點就是新建線程的生命周期不得長於std::thread變量的生命周期。否則std::thread變量在出了作用域后會自動調用析構函數,導致新建線程退出。

線程的阻塞和分離

在運行一個線程后,如果我們阻塞當前線程來等待新建線程運行完成。

int main() {
    std::thread t(func);
    t.join();
}

這樣,主線程就會被阻塞,直到func函數調用返回才繼續運行。

我們也可以通過detach函數讓新建線程與當前線程進行分離,讓新建線程到后台運行。新建線程與主線程分離后其生命周期自然也不再受到std::thread生命周期的限制。

int main() {
    std::thread t(func);
    t.detach();
    //keep running
}

線程的移動

通過std::move函數可以移動一個線程變量

std::thread t(func);
std::thread t1(std::move(t));

移動之后,t變量無法再使用。

獲取當前線程

通過std::this_thread變量可以指向當前線程。

互斥量

互斥量是一種用於線程同步的手段。

4種語義的互斥量被提供:

  • std::mutex
  • std::timed_mutex: 帶超時的互斥量
  • std::recursive_mutex: 可遞歸互斥量
  • std::recursive_timed_mutex

這里解釋一下,什么叫可遞歸互斥量。

可遞歸互斥量,其實就是允許一個線程多次獲取互斥量的互斥量。因為在一般的互斥量中,如果一個線程在已經獲取了互斥量的情況下再次阻塞式地獲取互斥量就會導致死鎖。

而可遞歸互斥量允許多次獲取互斥量的行為,避免了死鎖。

互斥量的使用

由於四種互斥量都提供了接近統一的接口

  • 通過lock()函數阻塞式地獲取互斥量。如果是設置的超時互斥量,則超過時間后就不會再阻塞。
  • 通過unlock()函數釋放互斥量
  • 通過try_lock()函數非阻塞式地獲取一個互斥量。如果成功獲取就返回true,否則返回false。

通過lock_guard類型進行互斥量的統一管理

與指針類似,互斥量這種需要手動獲取和釋放的資源可以通過RAII(Resource Acquisition is Initialization)技術進行統一管理。

lock_guard的初始化方法為:

std::lock_guard<std::mutex> locker(g_lock); //g_lock是std::mutex類型變量

locker變量出了作用域后就會自動釋放互斥量。

條件變量

條件變量是另一種同步機制,經常與互斥量配合使用。

考慮這樣一種場景,某一條線程需要臨界資源滿足某一種條件才對臨界資源進行操作,如 搶票系統要在票賣完后進行加票。則加票的線程需要一直進行互斥鎖的搶占,搶占后只是 檢查票是否賣完了,但大部分時間是沒賣完的,所以多了很多不必要的搶占。而在票真正 賣完后,加票線程又可能需要搶占多輪才能搶到互斥鎖,導致程序的延誤。這些都是只用互斥鎖的程序很難解決的。

而如果我們引入條件變量,則可以轉變下面這種場景:如果是取票線程搶占到了互斥量,則檢查是否沒票了,如果是,則令其等待一個條件變量m_notEmpty。如果是放票線程搶占到了互斥量,則檢查是否還有票。如果是,則令其等待另一個條件變量m_empty。

為此,取票和放票的線程可以定義為:

void put(const T& x) {
    std::lock_guard<std::mutex> locker(m_mutex);
    while (!is_empty()) {
        cout << "票還有,等待..." << endl;
        //wait函數會阻塞當前線程,使得當前線程不參與搶占。
        m_empty.wait(m_mutex);
    }
    pool.push_back(x);
    //下面這個函數會喚醒所有等待m_notEmpty條件變量的線程
    m_notEmpty.notify_all();
}

void take(const T& x) {
    std::lock_guard<std::mutex> locker(m_mutex);
    while (is_empty()) {
        cout << "沒票了,等待..." << endl;
        m_notEmpty.wait(m_mutex);
        m_empty.notify_one(); //喚醒放票線程
    }
    x = pool.front();
    pool.pop_front();
}

由這個例子看來,條件變量是用於當前線程主動放棄搶占的工具。當我們知道在不滿足某些條件時,即使搶占到了互斥量也沒用。這時該線程可選擇主動阻塞,並聯系到一個條件變量上。這樣,其他沒有阻塞在進行一系列操作后可以通過notify_one或者notify_all方法將當前線程重新喚醒。

wait函數也可以這樣寫

m_noEmpty.wait(m_mutex, [this]{return !is_empty();});

原子變量

C++11提供了一個原子變量類型 std::atomic<T>。

通常在修改一個變量時包含了三個步驟:讀取,修改,回寫。

在並發條件下,可能這三個步驟中摻雜了其他線程的操作。而原子變量可以保證這三個步驟在一個線程內同時完成。避免了互斥量的使用。

異步操作函數async

有時你可能需要通過另一個線程來完成一個簡單的工作,但是又不想煞費苦心地創建一個線程。並且你可能還希望這個線程能夠返回一個結果給你。

於是C++11給你提供了std::async函數,這個函數的使用就像調用一個普通函數一樣,只是這個函數由另一個線程去執行。

函數原型為

std::future res = std::async(std::launch::async|std::launch::deferred|std::launch, func, args...);

第一個參數表示創建的時機,std::launch::async(默認)表示在調用async函數時就創建線程。std::launch::defeerred表示在返回值調用get或wait成員函數才創建線程。

第二個參數自然是函數或者說可調用對象都可以,第三個參數表示函數參數。

現在說一下返回值,返回值類型是std::future。future表示這個結果只有在未來的某時刻才會求出。future除了包含了線程的返回值外,還包含了線程的運行狀態。包括三種狀態:

  • std::future_status::deferred: 異步操作還沒開始
  • std::future_status::ready: 異步操作已經完成
  • std::future_status::timeout: 異步操作超時

通過wait_for啟動一個線程

前面說過,async函數運行的線程可以等到返回值調用wait_for或wait函數或get函數調用時再創建。

wait和wait_for函數的作用都差不多,就是阻塞當前線程來等待future對應的線程的運行結果。區別就是wait_for函數可以設置超時時間。

需要注意的是future類型變量不可被拷貝,只能被移動。

另外,async函數還是與協程有挺大的區別。協程是創建一個線程並將自己的CPU時間讓給它,而async函數只是創建一個線程放到后台運行。不過聽說C++20也開始支持協程了。


免責聲明!

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



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