別再被多執行緒搞暈了!一篇文章輕鬆搞懂 Linux 多執行緒同步!

小康發表於2024-11-06

前言

大家有沒有遇到過,程式碼跑著跑著,執行緒突然搶資源搶瘋了?其實,這都是“多執行緒同步”在作怪。多執行緒同步是個老生常談的話題,可每次真正要處理時還是讓人頭疼。這篇文章,帶你從頭到尾掌握 Linux 的多執行緒同步,把概念講成大白話,讓你看了不再迷糊,還能拿出來裝一裝逼!不管是“鎖”、“訊號量”,還是“條件變數”,我們都一網打盡,趕緊點贊收藏,一文搞懂!

一、什麼是執行緒同步?——“排隊來操作,按規矩走”

執行緒同步的核心,就是控制多個執行緒的訪問順序,讓它們在訪問共享資源時有序、穩定。你可以把它想象成大家排隊進電影院,每個執行緒都是觀眾,排好隊才能有序進場。如果大家一擁而上,不僅容易出事,還誰也看不成電影。

簡單來說,執行緒同步就是一個“排隊工具”,讓執行緒們按順序、按規則去操作資源,避免混亂、出錯。

二、 為什麼需要多執行緒同步?——不想大家打架就得“排好隊”

簡單來說,多執行緒同步就是為了控制多個執行緒之間的訪問順序,保證資料的一致性,防止執行緒“打架”。
比如你有多個執行緒在“搶”同一個變數,它們隨時會互相影響,最終導致程式結果錯得一塌糊塗,甚至程式崩潰。這時候就像幾個朋友圍在一桌,大家都想夾最後一塊肉,結果誰也夾不到,甚至還打起來了!在計算機中,這個場景會導致資源衝突或者死鎖。

三、執行緒同步的常見問題?

為什麼多執行緒容易“打架”?因為執行緒是獨立的執行單元,它們的執行順序不確定。幾個常見的問題:

  • 競爭條件: 多個執行緒同時搶著用同一個資源,結果資料出錯、搞亂了。
  • 死鎖: 執行緒互相等待彼此的資源,誰也不讓誰,最後都卡在那兒不動了。
  • 活鎖: 執行緒為了避免衝突,不停地讓來讓去,結果誰也沒法繼續工作,任務一直停滯著。

所以,為了保證程式的正確性、資料一致性,Linux 提供了各種同步工具。可以理解為“排隊工具”,讓執行緒一個一個地來,用完再走,大家和平共處。

四、同步工具集錦:全家福

在 Linux 中,常用的同步工具主要有七類:

  • 互斥鎖(Mutex):一人一次,誰拿到誰操作,別搶!
  • 條件變數(Condition Variable):有人負責通知,其他人等訊號,一喊開工就一哄而上。
  • 訊號量(Semaphore):有限名額,控制同時訪問資源的執行緒數量,適合多執行緒限流。
  • 讀寫鎖(Reader-Writer Lock):有讀有寫,讀可以多人一起看,寫得自己來。
  • 自旋鎖(Spin Lock):不停地檢查鎖,忙等。適合短時間鎖定場景。
  • 屏障(Barrier):所有執行緒到這兒集合,等到齊了一起開始下一步。
  • 原子操作(Atomic Operations):小資料更新直接操作,不加鎖,速度快,適合簡單計數和標誌位更新。

這些工具看起來好像有點複雜,但咱們一個一個來,保你一學就懂!

五、互斥鎖(Mutex):誰拿到,誰先操作

互斥鎖是多執行緒同步的基礎。顧名思義,互斥鎖(mutex)是一種獨佔機制,即一次只允許一個執行緒訪問共享資源。要理解互斥鎖的作用,可以想象一下“廁所上鎖”的場景:假設家裡有一個衛生間,進門時必須鎖上,完事出來再開鎖,以防別人誤闖。

常見介面:

在 POSIX 執行緒庫中,互斥鎖透過 pthread_mutex_t 型別實現,提供了以下常見介面:

  • pthread_mutex_init(&mutex, nullptr):初始化互斥鎖
  • pthread_mutex_lock(&mutex):加鎖,若已被其他執行緒鎖定,則阻塞等待
  • pthread_mutex_trylock(&mutex):嘗試加鎖,若鎖已被佔用,則立即返回錯誤而不阻塞
  • pthread_mutex_unlock(&mutex):解鎖,釋放互斥鎖,允許其他執行緒加鎖
  • pthread_mutex_destroy(&mutex):銷燬互斥鎖,釋放相關資源

簡單程式碼示例:

這段程式碼展示瞭如何使用互斥鎖(mutex)來確保多個執行緒對共享變數 counter 的安全訪問。

#include <pthread.h>
#include <iostream>

pthread_mutex_t mutex;  // 宣告互斥鎖

int counter = 0;

void* increment(void* arg) {
    pthread_mutex_lock(&mutex);  // 加鎖
    counter++;
    pthread_mutex_unlock(&mutex);  // 解鎖
    return nullptr;
}

int main() {
    pthread_t t1, t2;
    pthread_mutex_init(&mutex, nullptr);  // 初始化互斥鎖

    pthread_create(&t1, nullptr, increment, nullptr);
    pthread_create(&t2, nullptr, increment, nullptr);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);

    std::cout << "Final counter value: " << counter << std::endl;

    pthread_mutex_destroy(&mutex);  // 銷燬互斥鎖
    return 0;
}

程式碼解釋:

increment 函式:每個執行緒呼叫此函式,對 counter 變數進行加 1 操作。為了防止多個執行緒同時修改 counter,使用了互斥鎖:

  • pthread_mutex_lock(&mutex):加鎖,確保只有一個執行緒可以修改 counter
  • counter++:增加 counter 的值
  • pthread_mutex_unlock(&mutex):解鎖,允許其他執行緒訪問

主函式 main

  • pthread_mutex_init(&mutex, nullptr):初始化互斥鎖
  • 建立兩個執行緒 t1 和 t2,它們都執行 increment 函式
  • pthread_join 等待 t1 和 t2 結束
  • 列印 counter 的最終值
  • pthread_mutex_destroy(&mutex):銷燬互斥鎖,釋放資源

透過互斥鎖的加鎖和解鎖,程式碼確保了兩個執行緒不會同時修改 counter,從而保證資料安全。

優缺點

優點:

  • 簡單高效:互斥鎖的加鎖和解鎖操作非常簡單,執行效率高,適合需要短時間鎖定資源的場合。
  • 資料安全:互斥鎖可以保證同一時刻只有一個執行緒訪問共享資源,避免資料衝突,保證資料的一致性。
  • 防止資源爭搶:互斥鎖確保資源不被多個執行緒同時訪問,從而避免競爭帶來的資料錯誤或程式崩潰。

缺點:

  • 阻塞其他執行緒:一旦資源被鎖定,其他執行緒只能等待,這可能導致系統效率降低,尤其是鎖定時間較長時。
  • 存在死鎖風險:如果兩個執行緒互相等待對方釋放鎖,就可能導致死鎖。因此設計鎖的使用順序時需要格外小心。
  • 不適合長時間鎖定:互斥鎖適合短期操作,鎖定時間過長會影響程式的併發性,因為其他執行緒在等待鎖時會被阻塞,降低系統效能。

應用場景:

互斥鎖適合那些需要獨佔資源訪問的情況,比如多個執行緒同時需要修改同一個變數、更新配置檔案、寫檔案等操作。互斥鎖確保這些操作不會被打斷,資源在操作時“鎖”住,保證訪問的有序和安全性。

六、條件變數(Condition Variable): 有訊號才行動

條件變數有點像“等通知”。一個執行緒負責等訊號,另一個執行緒發出訊號。比如生產者和消費者,消費者要等到有貨了才能繼續;生產者一旦備好了貨,就發個訊號給消費者,“來吧,過來取,貨到齊了!”

常見介面:

在 POSIX 執行緒庫中,條件變數透過 pthread_cond_t 型別實現,配合互斥鎖使用,常見介面包括以下幾種:

  • pthread_cond_init(&cond, nullptr):初始化條件變數。
  • pthread_cond_wait(&cond, &mutex):等待條件變數。需要持有互斥鎖,當條件不滿足時自動釋放鎖並進入等待狀態,直到接收到訊號或被喚醒。
  • pthread_cond_signal(&cond):傳送訊號,喚醒一個正在等待的執行緒。適用於通知單個等待執行緒的情況。
  • pthread_cond_broadcast(&cond):廣播訊號,喚醒所有正在等待的執行緒。
  • pthread_cond_destroy(&cond):銷燬條件變數,釋放相關資源。

簡單程式碼示例:

這段程式碼展示瞭如何使用 條件變數(Condition Variable) 和 互斥鎖(Mutex) 來協調兩個執行緒之間的同步。程式碼中有兩個執行緒,一個執行緒在等待訊號,另一個執行緒傳送訊號。

#include <pthread.h>
#include <iostream>

pthread_mutex_t mutex; // 宣告互斥鎖
pthread_cond_t cond;   // 宣告條件變數
bool ready = false;

void* waitForSignal(void* arg) {
    pthread_mutex_lock(&mutex);
    while (!ready) {
        pthread_cond_wait(&cond, &mutex);  // 等待條件變數的訊號
    }
    std::cout << "Signal received!" << std::endl;
    pthread_mutex_unlock(&mutex);
    return nullptr;
}

void* sendSignal(void* arg) {
    pthread_mutex_lock(&mutex);
    ready = true;
    pthread_cond_signal(&cond);  // 傳送訊號
    pthread_mutex_unlock(&mutex);
    return nullptr;
}

int main() {
    pthread_t t1, t2;
    pthread_mutex_init(&mutex, nullptr); // 初始化互斥鎖
    pthread_cond_init(&cond, nullptr);   // 初始化條件變數

    pthread_create(&t1, nullptr, waitForSignal, nullptr);
    pthread_create(&t2, nullptr, sendSignal, nullptr);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);

    pthread_mutex_destroy(&mutex);   // 銷燬條件變數
    pthread_cond_destroy(&cond);     // 銷燬條件變數
    return 0;
}

程式碼解釋:

  • waitForSignal 函式:等待訊號的執行緒,加鎖後檢查ready的狀態。如果readyfalse,執行緒會呼叫pthread_cond_wait進入等待狀態,直到收到sendSignal執行緒的訊號才繼續執行。
  • sendSignal 函式:傳送訊號的執行緒,先加鎖,將ready設為true,然後呼叫pthread_cond_signal通知等待執行緒可以繼續。最後解鎖,讓waitForSignal執行緒繼續執行。
  • 主函式 main: 初始化互斥鎖和條件變數,建立兩個執行緒t1t2,分別執行等待和傳送訊號的任務,最後等待執行緒完成並銷燬互斥鎖和條件變數。

優缺點:

優點:

  • 減少忙等:使用條件變數可以讓執行緒進入等待狀態,不消耗 CPU 資源,等待到達訊號再繼續執行,提升效率。
  • 多執行緒協作更有序:條件變數使執行緒之間的配合更有序,避免資源的無效爭搶。
  • 支援多執行緒喚醒:條件變數的廣播功能可以一次喚醒多個執行緒,非常適合需要同步的多執行緒場景。

缺點:

  • 需要互斥鎖配合:條件變數不能單獨使用,必須與互斥鎖一起使用,增加了編寫的複雜度。
  • 可能出現虛假喚醒pthread_cond_wait 可能會出現“虛假喚醒”情況,因此需要在迴圈中反覆檢查條件是否滿足。
  • 程式設計複雜度增加:對於新手來說,條件變數與互斥鎖的搭配使用會增加多執行緒程式設計的難度。

應用場景:

條件變數適用於生產者-消費者模型等場景,非常適合一個執行緒需要等待另一個執行緒完成某些操作的情況,比如等待任務完成、資源釋放、資料處理等。透過條件變數,一個執行緒可以在等待條件達成時自動暫停,等收到訊號後再繼續執行。

七、訊號量(Semaphore):誰來誰得,有限名額

訊號量就像門口的限流器。允許一定數量的執行緒同時進入“臨界區”(共享資源區),超過這個數量的執行緒就得在門口等著。比如限量版奶茶店,一次只能進五個人,想喝就得排隊!

常見介面:

在 POSIX 執行緒庫中,訊號量透過 sem_t 型別實現,介面主要包括:

  • sem_init(&semaphore, 0, count):初始化訊號量,count 是訊號量初始值,表示同時允許進入的執行緒數量。
  • sem_wait(&semaphore):請求資源。當訊號量大於零時,減一併進入臨界區;若訊號量為零,則執行緒阻塞,直到其他執行緒釋放資源。
  • sem_post(&semaphore):釋放資源,增加訊號量值,允許其他等待的執行緒繼續。
  • sem_destroy(&semaphore):銷燬訊號量,釋放資源。

簡單程式碼示例:

下面的程式碼展示瞭如何使用訊號量來控制多個執行緒對資源的訪問許可權。在這個例子中,訊號量初始值為 1,確保同一時間只有一個執行緒能進入臨界區。

#include <semaphore.h>
#include <pthread.h>
#include <iostream>

sem_t semaphore;

void* accessResource(void* arg) {
    sem_wait(&semaphore);  // 請求資源
    std::cout << "Thread accessing resource!" << std::endl;
    sem_post(&semaphore);  // 釋放資源
    return nullptr;
}

int main() {
    pthread_t t1, t2;
    sem_init(&semaphore, 0, 1);  // 初始化訊號量,允許1個執行緒訪問資源

    pthread_create(&t1, nullptr, accessResource, nullptr);
    pthread_create(&t2, nullptr, accessResource, nullptr);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);

    sem_destroy(&semaphore);  // 銷燬訊號量
    return 0;
}

程式碼解釋:

  • sem_wait(&semaphore);:請求訪問資源,訊號量減一。如果訊號量為零,執行緒將等待。
  • sem_post(&semaphore);:釋放資源,訊號量加一,讓其他等待的執行緒可以進入。

主函式中,兩個執行緒 t1t2 會分別呼叫 accessResource。訊號量初始值設為 1,保證同一時刻只有一個執行緒訪問資源,避免衝突。

優缺點

優點

  • 控制併發量:訊號量允許多個執行緒同時進入,特別適合一些允許並行讀的場景,比如檔案讀寫或資料庫連線池。
  • 靈活性強:訊號量不僅支援單執行緒進入,還支援多執行緒進入。

缺點

  • 不易程式設計和除錯:由於訊號量的計數器機制,容易導致邏輯混亂,程式設計複雜且除錯較難。
  • 不能識別優先順序:訊號量沒有內建的優先順序佇列,某些等待時間長的執行緒可能會“餓死”。

應用場景:

  • 限流:例如資料庫連線池中限制同時連線數,透過訊號量控制最大連線數。
  • 讀寫分離:讀操作允許多個執行緒同時進行,而寫操作需要獨佔訪問。
  • 共享資源管理:如資源池、任務佇列等,有固定容量的資源池中允許多個執行緒訪問,但超過容量則需等待。

訊號量在限制併發時非常實用,能夠靈活控制執行緒數量,特別適合一些讀寫分離或限流場景,是多執行緒同步中的“好幫手”。

八、讀寫鎖(Reader-Writer Lock):讀可以一起,寫得單獨

讀寫鎖的作用顧名思義,就是讓“讀”操作更輕鬆。在多執行緒場景中,多個執行緒可以同時讀取資源(共享檢視),但寫操作必須獨佔,確保不會在讀取時被其他執行緒修改內容。這就像圖書館的書,大家可以一起看,但如果有人要修改書的內容,就得把書借走,防止其他人讀到一半內容突然變了。

常見介面 :

  • pthread_rwlock_init(&rwlock, nullptr)初始化讀寫鎖。在使用讀寫鎖之前必須初始化,可以選擇設定鎖的屬性(用 nullptr 表示預設屬性)。
  • pthread_rwlock_rdlock(&rwlock)加讀鎖。多個執行緒可以同時持有讀鎖,但如果有執行緒持有寫鎖,呼叫執行緒會被阻塞,直到寫鎖釋放。
  • pthread_rwlock_wrlock(&rwlock)加寫鎖。加寫鎖時,執行緒需獨佔讀寫鎖。持有寫鎖期間,所有其他的讀鎖或寫鎖請求都會被阻塞,直到寫鎖被釋放。
  • pthread_rwlock_unlock(&rwlock)解鎖。無論是讀鎖還是寫鎖,都可以使用該介面解鎖。若當前持有讀鎖,則釋放一個讀鎖;若持有寫鎖,則釋放寫鎖,允許其他執行緒加鎖。
  • pthread_rwlock_destroy(&rwlock)銷燬讀寫鎖。在不再需要使用讀寫鎖時銷燬它,釋放相關的資源。

簡單程式碼示例:

這段程式碼展示了讀寫鎖(rwlock)的基本用法,目的是讓多個執行緒同時訪問共享變數 counter,並確保讀取和寫入操作的安全性。

#include <pthread.h>
#include <iostream>

pthread_rwlock_t rwlock;  // 宣告讀寫鎖

int counter = 0;

void* readCounter(void* arg) {
    pthread_rwlock_rdlock(&rwlock);  // 加讀鎖
    std::cout << "Counter: " << counter << std::endl;
    pthread_rwlock_unlock(&rwlock);  // 解鎖
    return nullptr;
}

void* writeCounter(void* arg) {
    pthread_rwlock_wrlock(&rwlock);  // 加寫鎖
    counter++;
    pthread_rwlock_unlock(&rwlock);  // 解鎖
    return nullptr;
}

int main() {
    pthread_t t1, t2, t3;
    pthread_rwlock_init(&rwlock, nullptr);  // 初始化讀寫鎖

    pthread_create(&t1, nullptr, readCounter, nullptr);
    pthread_create(&t2, nullptr, writeCounter, nullptr);
    pthread_create(&t3, nullptr, readCounter, nullptr);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);

    pthread_rwlock_destroy(&rwlock);  // 銷燬讀寫鎖
    return 0;
}

程式碼解釋:

  • readCounter 函式:獲取讀鎖 pthread_rwlock_rdlock,讀取 counter 的值並列印,然後釋放讀鎖。多個執行緒可以同時獲取讀鎖,允許併發讀取。
  • writeCounter 函式:獲取寫鎖 pthread_rwlock_wrlock,增加 counter 的值,然後釋放寫鎖。寫鎖是獨佔的,同一時間只有一個執行緒可以寫入 counter
  • main 函式:建立了三個執行緒 t1t2t3,兩個執行緒進行讀取操作(readCounter),一個執行緒進行寫入操作(writeCounter)。讀寫鎖 rwlock 確保了讀取和寫入時的執行緒安全。

優缺點

優點:

  • 高效的讀操作:多個執行緒可以同時讀取資源,不會互相阻塞,避免了因互斥鎖導致的效率低下。
  • 寫操作安全:寫操作獨佔鎖,確保資料不會因為讀寫交叉而出錯。

缺點:

  • 可能導致“寫飢餓”:如果一直有執行緒在讀取,寫執行緒可能一直無法獲取鎖,導致寫操作被延遲。
  • 不適合頻繁寫的場景:在寫操作多的情況下,讀寫鎖的優勢不明顯,反而因為鎖的開銷影響效能。

應用場景:

  • 日誌和配置讀取: 日誌內容可以被多個執行緒同時讀取,但在寫日誌或更新配置時需要獨佔。
  • 快取系統:例如計數器等共享資源,多執行緒環境中讀多寫少的快取操作特別適合讀寫鎖。
  • 統計資料系統: 資料讀取頻繁而寫入較少的統計系統中,讀寫鎖能提供更高的讀取效率。

九、自旋鎖(Spinlock):等不到就原地打轉

自旋鎖是種“忙等”鎖,不獲取到鎖,它就原地打轉,一直“自旋”等待。自旋鎖適合短時間加鎖的場景,時間一長就耗CPU了,所以常用於等待時間極短的資源。因此,自旋鎖經常用於等待時間非常短的資源訪問場景。

常見介面 :

  • pthread_spin_init(pthread_spinlock_t* lock, int pshared)
    初始化自旋鎖,引數pshared指定自旋鎖是否在程序間共享(0表示僅在程序內使用)。如果成功返回0,否則返回錯誤程式碼。
  • pthread_spin_lock(pthread_spinlock_t* lock)
    加鎖操作,嘗試獲取自旋鎖。如果鎖已經被佔用,當前執行緒會一直迴圈等待,直到獲取鎖。
  • pthread_spin_unlock(pthread_spinlock_t* lock)
    解鎖操作,釋放自旋鎖,讓其他執行緒可以繼續嘗試獲取鎖。
  • pthread_spin_destroy(pthread_spinlock_t* lock)
    銷燬自旋鎖,釋放資源。呼叫此函式後不能再使用該鎖,除非重新初始化。

簡單程式碼示例:

下面的程式碼展示瞭如何使用自旋鎖在兩個執行緒間進行資源訪問控制,確保 counter 的安全遞增。

#include <pthread.h>
#include <iostream>

pthread_spinlock_t spinlock;  // 宣告自旋鎖
int counter = 0;

void* increment(void* arg) {
    pthread_spin_lock(&spinlock);  // 加鎖
    counter++;
    pthread_spin_unlock(&spinlock);  // 解鎖
    return nullptr;
}

int main() {
    pthread_t t1, t2;

    pthread_spin_init(&spinlock, 0);  // 初始化自旋鎖

    // 建立兩個執行緒,分別執行 increment 函式
    pthread_create(&t1, nullptr, increment, nullptr);
    pthread_create(&t2, nullptr, increment, nullptr);

    // 等待兩個執行緒執行完畢
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);

    std::cout << "Final counter value: " << counter << std::endl;

    pthread_spin_destroy(&spinlock);  // 銷燬自旋鎖
    return 0;
}

程式碼解釋:

increment 函式:
每個執行緒呼叫此函式,對counter進行加1操作。為了確保執行緒安全,使用了自旋鎖spinlock

  • pthread_spin_lock(&spinlock):加鎖,使當前執行緒獨佔訪問counter
  • counter++:增加counter的值。
  • pthread_spin_unlock(&spinlock):解鎖,讓其他執行緒可以訪問counter

主函式 main

  • pthread_create(&t1, nullptr, increment, nullptr)pthread_create(&t2, nullptr, increment, nullptr):建立兩個執行緒t1t2,分別執行increment函式。
  • pthread_join(t1, nullptr)pthread_join(t2, nullptr):等待t1t2執行完畢。

透過自旋鎖,這段程式碼確保了兩個執行緒不會同時修改counter,保證了資料安全。

優缺點

優點

  • 減少上下文切換:自旋鎖不會讓執行緒進入“阻塞等待”,而是讓執行緒“忙等”來獲取鎖。這樣避免了執行緒進入“睡眠-喚醒”的過程(即“上下文切換”),使得等待過程更快。
  • 適合短時間鎖定:自旋鎖適合那些等待時間極短的情況,因為在這種情況下,等待時間和“忙等”的成本低於切換上下文的開銷。

缺點

  • 在高競爭環境下效能下降:如果多個執行緒同時競爭同一個鎖,自旋鎖的“忙等”會導致大量執行緒佔用 CPU,最終讓 CPU 資源被浪費,導致效能下降。
  • 不適合長時間鎖定:如果持有鎖的時間較長,執行緒會在等待期間不斷佔用 CPU,造成資源浪費。因此,自旋鎖只適合持鎖時間非常短的場景。

應用場景:

適合短時、高頻鎖的情況:在多核 CPU 上,自旋鎖非常適合那些“鎖定時間極短但加鎖頻繁”的情況,比如快速更新某個標誌位、計數器等。這種操作速度快、鎖的持有時間短,因此用自旋鎖可以減少阻塞帶來的上下文切換開銷。

十、屏障(Barrier):到齊了就開工

屏障的作用是讓一組執行緒都到達某個集合點,然後再一起繼續。可以把它看作一個“集合點”,每個執行緒到這兒後必須等一等,直到所有執行緒都到齊,然後才能一起“放行”。這在需要同步的多執行緒任務中特別有用,比如並行的資料處理:每一階段的資料處理需要多個執行緒完成,各自到達指定點後,才能一起進入下一個階段。

常見介面:

在 POSIX 執行緒庫中,屏障透過 pthread_barrier_t 型別實現,常用介面包括以下幾個:

  • pthread_barrier_destroy(&barrier):銷燬屏障,釋放資源,通常在程式結束時呼叫。
  • pthread_barrier_init(pthread_barrier_t* barrier, const pthread_barrierattr_t* attr, unsigned count) :初始化屏障,count引數指定屏障需要等待的執行緒數量。到達count個執行緒後,屏障會放行所有等待的執行緒。
  • pthread_barrier_wait(pthread_barrier_t* barrier) :執行緒呼叫此函式後進入等待狀態,直到所有執行緒都呼叫了這個函式,屏障才會釋放執行緒進入下一步操作。
  • pthread_barrier_destroy(pthread_barrier_t* barrier) :銷燬屏障,釋放相關資源。

簡單程式碼示例:屏障同步

下面的程式碼展示瞭如何使用屏障讓三個執行緒同步等待,等到三個執行緒都到達屏障點後再繼續執行。這樣可以確保每個執行緒都在同一個步驟上同步。

#include <pthread.h>
#include <iostream>

pthread_barrier_t barrier;  // 宣告屏障

void* waitAtBarrier(void* arg) {
    std::cout << "Thread waiting at barrier..." << std::endl;
    pthread_barrier_wait(&barrier);  // 等待屏障
    std::cout << "Thread passed the barrier!" << std::endl;
    return nullptr;
}

int main() {
    pthread_t t1, t2, t3;

    // 初始化屏障,3個執行緒需要同步
    pthread_barrier_init(&barrier, nullptr, 3);

    // 建立執行緒
    pthread_create(&t1, nullptr, waitAtBarrier, nullptr);
    pthread_create(&t2, nullptr, waitAtBarrier, nullptr);
    pthread_create(&t3, nullptr, waitAtBarrier, nullptr);

    // 等待執行緒結束
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);

    pthread_barrier_destroy(&barrier);  // 銷燬屏障
    return 0;
}

程式碼解釋:

waitAtBarrier 函式:每個執行緒在此函式中執行,先列印“Thread waiting at barrier...”表示到達屏障,然後呼叫 pthread_barrier_wait(&barrier) 在屏障處等待,直到所有執行緒都到達,之後才繼續執行並列印“Thread passed the barrier!”。

主函式 main

  • pthread_barrier_init(&barrier, nullptr, 3);:初始化屏障,要求 3 個執行緒同步到達。
  • 建立了 3 個執行緒(t1, t2, t3),它們都呼叫 waitAtBarrier 函式。
  • pthread_join 等待所有執行緒完成。
  • pthread_barrier_destroy(&barrier);:銷燬屏障,釋放資源。

這段程式碼的效果是:3 個執行緒都會在屏障處等待,直到全部執行緒到達後再一起透過,確保同步執行。

優缺點

優點

  • 簡化階段性同步:屏障特別適合多執行緒任務中的分階段同步,比如大規模資料分批處理,每批資料處理完,所有執行緒集齊後再進入下一階段。
  • 簡單易用:在需要多個執行緒同步的場景中,屏障提供了一個簡單的方案,避免了手動計數和鎖的複雜性。

缺點

  • 不靈活:屏障初始化時需要指定同步的執行緒數,在執行中無法動態更改,這在一些執行緒數變化的場景中可能不夠靈活。
  • 資源浪費:屏障需要等待所有執行緒到齊才能繼續,若某些執行緒執行速度慢,會導致其他執行緒在等待時浪費 CPU 資源。
  • 容易形成死鎖:如果有執行緒沒有到達屏障點,其他執行緒會一直等待,可能導致整個系統的執行緒死鎖。

應用場景:

屏障適用於需要同步階段的場合,尤其是以下幾種:

  • 分步資料處理:在資料處理中,有些步驟需要所有執行緒同步完成後再進入下一步。
  • 階段性任務同步:對於一些分階段的任務,每一步都需要多個執行緒協同完成,比如平行計算中的同步步驟。
  • 多執行緒計算匯合:比如科學計算、資料聚合等任務,每個執行緒完成部分任務後需要在屏障點集合彙總。

十一、原子操作(Atomic Operations):小塊更新,快準狠

原子操作是一種“小而快”的多執行緒操作。它直接對資料進行“獨佔式”的更新,操作不可分割,不需要加鎖,因為它的操作是原子的:要麼全做,要麼全不做。適合用於快速更新小資料,比如計數、標誌位等場景。在多執行緒環境中使用原子操作,可以避免加鎖帶來的效能開銷,因此更新簡單共享資源時,非常高效。

常見介面:

在C++的標準庫中,原子操作介面非常簡單,常用的有以下幾種:

  • std::atomic<T>
    宣告一個原子型別的變數T,常用於簡單資料型別,如intbool等。std::atomic<int> counter(0);表示一個整型原子變數counter,初始值為0。
  • fetch_add()fetch_sub()
    分別用於原子加和原子減操作,例如counter.fetch_add(1);會安全地加1,同時返回舊值。
  • load()store()
    load()用於原子地讀取變數值,store()用於原子地儲存值,確保資料的一致性。

簡單程式碼示例:原子操作實現計數器

下面的程式碼展示瞭如何使用 std::atomic 來安全地對共享資料 counter 進行遞增操作。此處無需加鎖,原子操作自動確保了執行緒安全。

#include <atomic>
#include <pthread.h>
#include <iostream>

std::atomic<int> counter(0);  // 使用原子型別

void* increment(void* arg) {
    for (int i = 0; i < 100000; ++i) {
        counter++;  // 原子操作,自動保證執行緒安全
    }
    return nullptr;
}

int main() {
    pthread_t t1, t2;

    // 建立兩個執行緒
    pthread_create(&t1, nullptr, increment, nullptr);
    pthread_create(&t2, nullptr, increment, nullptr);

    // 等待兩個執行緒執行完畢
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);

    std::cout << "Final counter value: " << counter << std::endl;
    return 0;
}

程式碼解釋:

  • std::atomic<int> counter(0);:使用原子型別 std::atomic 宣告計數器 counter。所有對 counter 的操作都是執行緒安全的。
  • counter++:原子遞增操作,無需加鎖,在多執行緒環境下也能保證資料的一致性。

透過原子操作,我們避免了加鎖帶來的效能開銷,程式碼簡潔、高效,特別適合對小資料的頻繁更新。

優缺點

優點

  • 無需加鎖:原子操作是天然的執行緒安全操作,不需要額外的鎖機制。
  • 效能高:原子操作減少了鎖開銷,效能更高,特別適合小範圍的更新操作。
  • 程式碼簡單:使用 std::atomic 可以直接更新共享資料,程式碼更簡潔。

缺點

  • 只適合簡單資料:原子操作適用於小資料的單個操作,無法用於複雜的資料結構或多步操作。
  • 不支援複雜同步:原子操作僅適合簡單的同步需求,比如計數、標誌位等,無法處理複雜的併發控制。
  • 可能影響可讀性:如果不熟悉原子操作的語義,程式碼的可讀性可能較低。

應用場景:

原子操作非常適合以下幾種場合:

  • 計數器:在多執行緒環境中,對計數器的增減操作非常高效,比如執行緒池中的任務計數。
  • 標誌位更新:更新多執行緒任務中的狀態標誌,比如任務是否完成、資源是否可用等。
  • 快速計數統計:在需要頻繁更新的場合,原子操作可以避免鎖帶來的效能開銷,提高統計速度。

總結:

今天我們一起探討了 Linux 中的多執行緒同步方式,從互斥鎖條件變數,再到訊號量、讀寫鎖以及自旋鎖、還有屏障和原子操作,逐一解鎖了每種同步方式的應用場景和優缺點。學會這些技巧後,寫多執行緒程式就不再讓人頭疼了! 同步其實並不神秘,只要掌握好這些基礎工具,你也能寫出流暢又安全的多執行緒程式 。

如果覺得有幫助,別忘了點贊和分享,關注我,我們一起學更多有趣的程式設計知識!已經掌握了這些同步方式? 那恭喜你!如果還沒完全弄明白,沒關係,歡迎在評論區留言,我們一起討論,確保你都能搞懂!

想更快找到我? 直接微信搜尋 「跟著小康學程式設計」,或者掃描下方二維碼關注,和一群愛學習的程式設計小夥伴一起成長吧!

關注我能學到什麼?

  • 這裡分享 Linux C、C++、Go 開發、計算機基礎知識 和 程式設計面試乾貨等,內容深入淺出,讓技術學習變得輕鬆有趣。
  • 無論您是備戰面試,還是想提升程式設計技能,這裡都致力於提供實用、有趣、有深度的技術分享。快來關注,讓我們一起成長!

怎麼關注我的公眾號?

非常簡單!掃描下方二維碼即可一鍵關注。

此外,小康最近建立了一個技術交流群,專門用來討論技術問題和解答讀者的疑問。在閱讀文章時,如果有不理解的知識點,歡迎大家加入交流群提問。我會盡力為大家解答。期待與大家共同進步!

相關文章