pthread 線程同步

雖然本身是做Android開發的, 但經常會用到C/C++, 最近項目中剛好通過線程同步解決了一個問題,線程知識應用太廣泛了, 所以在此記錄下關於C/C++中比較實用基礎知識, 本篇文章就說明一下pthread中線程同步的幾種方式.

pthread

pthread 即 POSIX threads, POSIX表示可移植操作系統接口(Portable Operating System Interface of UNIX), 所以pthread就是在這個標準下實現的線程, 廣泛用於類Unix操作系統上, 使用需要引入的頭文件爲 #include <pthread.h>基本使用示例代碼如下:

#include <stdio.h>
#include <pthread.h>

int count = 0;

void* test_func_a(void* ptr);

void main() {
    int loopCount = 5;
    pthread_t thread[5];
    char args[loopCount][10];
    for (int i = 0; i < loopCount; i++) {
        int err;
        sprintf(args[i], "Thread %d ", i);
        err = pthread_create(&thread[i], NULL, test_func_a, (void*) args[i]);
        if (err) {
            printf("create pthread failed ret:%d \n", err);
        }
    }
    for (int i = 0; i < loopCount; i++) {
        pthread_join(thread[i], NULL);
    }
}

void* test_func_a(void* ptr) {
    char* msg = (char*) ptr;
    count++;
    printf("%s value:%d \n", msg, count);
}

pthread創建很簡單, 調用pthread_create()即可, 函數定義如下:

int pthread_create(pthread_t * thread, 
                       const pthread_attr_t * attr,
                       void * (*start_routine)(void *), 
                       void *arg);

參數1: 存儲創建線程的id
參數2:一些線程屬性, 如果只是普通使用, 傳NULL
參數3: 函數指針, 即你要在此線程中運行的函數
參數4:用於傳遞參數到運行的函數中

注意:如果你是C/C++混合編程, 第三個參數在C++中只能傳遞全局函數或者類的靜態函數, 不能傳類的普通成員函數, 因爲普通類成員函數是屬於對象的, 類沒有實例化是不能使用這個函數的.

pthread_create()創建線程後, 線程會立即運行, 通過調用pthread_join()等待線程結束, 此函數會阻塞當前線程, pthread_join()成功返回後, 線程資源就會被釋放, 上面的示例代碼,編譯(編譯要加-pthread參數)運行後輸出結果是不確定的, 原因是多個線程沒有同步, 造成一些不可預料的結果發生, 其中某次輸出結果如下:

 $ ./test_sync
Thread 0  value:1
Thread 2  value:3
Thread 1  value:2
Thread 3  value:4
Thread 4  value:5

下面開始講線程同步的問題.

線程同步-Joins

pthread_join()大多數情況下都是用來結束線程的, 即退出程序釋放相關線程, 但也可以作爲簡單同步功能來使用, 比如將上面示例代碼修改爲如下方式:

#include <stdio.h>
#include <pthread.h>


int count = 0;

void* test_func_a(void* ptr);

void main() {
    int loopCount = 5;
    pthread_t thread[5];
    char args[loopCount][10];
    for (int i = 0; i < loopCount; i++) {
        int err;
        sprintf(args[i], "Thread %d ", i);
        err = pthread_create(&thread[i], NULL, test_func_a, (void*) args[i]);
        if (err) {
            printf("create pthread failed ret:%d \n", err);
        }
        //前一個線程結束後才運行下一個線程
        pthread_join(thread[i], NULL);
    }
    /*for (int i = 0; i < loopCount; i++) {
        pthread_join(thread[i], NULL);
    }*/
}

void* test_func_a(void* ptr) {
    char* msg = (char*) ptr;
    count++;
    printf("%s value:%d \n", msg, count);
}

這樣修改後每次結果都是確定的, 原因是我們等前一個線程運行完成後,才啓動下一個線程, 之前是一次性啓動, 運行結果每次都是:

 $ ./test_join
Thread 0  value:1
Thread 1  value:2
Thread 2  value:3
Thread 3  value:4
Thread 4  value:5

此方式在時間項目中使用場景有限, 很少使用.

線程同步-Mutexes

Mutex即互斥量, 如果上鎖後, 其他線程則無法獲得鎖導致線程阻塞, 直到鎖被釋放,才能再次獲得鎖進而執行相關代碼, 示例代碼:

#include <stdio.h>
#include <pthread.h>


int count = 0;
pthread_mutex_t mLock;

void* test_func_a(void* ptr);

void main() {
    int loopCount = 5;
    pthread_t thread[5];
    char args[loopCount][10];
    pthread_mutex_init(&mLock, NULL);
    for (int i = 0; i < loopCount; i++) {
        int err;
        sprintf(args[i], "Thread %d ", i);
        err = pthread_create(&thread[i], NULL, test_func_a, (void*) args[i]);
        if (err) {
            printf("create pthread failed ret:%d \n", err);
        }
    }
    for (int i = 0; i < loopCount; i++) {
        pthread_join(thread[i], NULL);
    }
    pthread_mutex_destroy(&mLock);
}

void* test_func_a(void* ptr) {
    pthread_mutex_lock(&mLock);
    char* msg = (char*) ptr;
    count++;
    printf("%s value:%d \n", msg, count);
    pthread_mutex_unlock(&mLock);
}

使用流程如下:
1.定義 pthread_mutex_t pthread_mutex_t mLock;

  1. 初始化有兩種方式, 調用pthread_mutex_init(&mLock, NULL);或者mLock = PTHREAD_MUTEX_INITIALIZER;效果一樣, 後者是通過定義的宏來實現的.
  2. 調用lock和unlock函數, 上鎖(獲得鎖)pthread_mutex_lock(&mLock);, 解鎖(釋放鎖) pthread_mutex_unlock(&mLock);, 同一時間只有一個線程能獲得該鎖, 被lock後, 其他線程調用lock函數會阻塞當前線程.
  3. 釋放定義的pthread_mutex_t 資源, 調用pthread_mutex_destroy(&mLock);

上面代碼運行結果如下:

 $ ./test_mutex
Thread 0  value:1
Thread 1  value:2
Thread 2  value:3
Thread 3  value:4
Thread 4  value:5

注意: 上面的例子只能保證value的值是按照預期從1變爲5的, 但並不能保證thread的運行順序,也就是說你運行的結果有可能是下面這樣的:

 $ ./test_mutex
Thread 0  value:1
Thread 2  value:2
Thread 1  value:3
Thread 3  value:4
Thread 4  value:5

原因是上面的例子並不能保證各個線程獲取鎖的順序, 因爲每個線程獲得鎖的優先級是相同的, 所以順序有可能每次都不一樣.

線程同步-Condition Variables

Condition Variables 即條件變量, 在線程同步使用過程中要配合上面的mutex進行使用, 實際多線程開發使用較多, 比如經典的生產者-消費者關係就可以通過Condition Variables來實現, 常用的場景爲滿足某個條件讓當前線程阻塞進行等待, 當其他線程滿足另一個條件後, 通知正在等待的線程進行工作.
下面通過一個簡單例子來說明下基本使用:

#include <stdio.h>
#include <pthread.h>


int count = 0;
pthread_mutex_t mLock;
pthread_cond_t mCond;

void* test_func_a(void* ptr);

void main() {
    int loopCount = 5;
    pthread_t thread[5];
    char args[loopCount][10];
    pthread_mutex_init(&mLock, NULL);
    pthread_cond_init(&mCond, NULL);
    for (int i = 0; i < loopCount; i++) {
        int err;
        sprintf(args[i], "Thread %d ", i);
        err = pthread_create(&thread[i], NULL, test_func_a, (void*) args[i]);
        if (err) {
            printf("create pthread failed ret:%d \n", err);
        }
    }
    printf("sleep 1s start \n");
    sleep(1);
    printf("sleep 1s end, call pthread_cond_signal() \n");
    pthread_mutex_lock(&mLock);
    //喚醒所有等待mCond的線程
    pthread_cond_broadcast(&mCond);
    //喚醒一個線程,如果當前有多個線程等待,
    //根據優先級和等待時間選擇其中一個線程進行喚醒
    //pthread_cond_signal(&mCond);
    pthread_mutex_unlock(&mLock);
    for (int i = 0; i < loopCount; i++) {
        pthread_join(thread[i], NULL);
    }
    pthread_mutex_destroy(&mLock);
    pthread_cond_destroy(&mCond);
}

void* test_func_a(void* ptr) {
    pthread_mutex_lock(&mLock);
    pthread_cond_wait(&mCond, &mLock);
    pthread_mutex_unlock(&mLock);
    char* msg = (char*) ptr;
    count++;
    printf("%s value:%d \n", msg, count);
}

使用流程和mutex差不多,如下:

  1. 定義 pthread_cond_t pthread_cond_t mCond;
  2. 初始化也和mutex一樣兩種方式 pthread_cond_init(&mCond, NULL);mCond = PTHREAD_COND_INITIALIZER;
  3. 讓當前線程阻塞進入等待狀態
    pthread_mutex_lock(&mLock);
    pthread_cond_wait(&mCond, &mLock);
    pthread_mutex_unlock(&mLock);

注意, 此處必須和mutex一起使用, 即調用pthread_cond_wait()這個函數本身要上鎖, 否則會產生不可預料異常.

  1. 喚醒等待此條件變量的線程
    pthread_mutex_lock(&mLock);
    //喚醒所有等待mCond的線程
    pthread_cond_broadcast(&mCond);
    //喚醒一個線程,如果當前有多個線程等待,
    //根據優先級和等待時間選擇其中一個線程進行喚醒
    //pthread_cond_signal(&mCond);
    pthread_mutex_unlock(&mLock);
  1. 釋放資源 pthread_cond_destroy(&mCond);

上面示例代碼基本邏輯是啓動五個線程, 默認開始就阻塞(等待mCond), 然後主線程sleep 1s後, 喚醒所有等待的線程, 此時5個線程會同時運行同一個函數, 輸出結果不可預料, 某次結果如下:

 $ ./test_cond
sleep 1s start
sleep 1s end, call pthread_cond_signal()
Thread 0  value:1
Thread 3  value:4
Thread 1  value:2
Thread 2  value:3
Thread 4  value:5

總結

根據我自己遇到的一些多線程問題, 我覺得多線程開發需要注意一下幾點:

  1. 不要以常規思維思考沒做過同步的一些代碼的運行結果, 很多結果你自己是沒法預料的.
  2. 寫代碼時思路要清晰, 有lock就要保證能在合適時機unlock, 不然很容易出現死鎖.
  3. 線程參數傳遞大多數是通過指針來實現的, 需要注意這些參數的生命週期, C/C++和Java不同,
    Java中只要有引用對象就不會被釋放, 但C/C++中則不同, 超出作用域或者手動釋放, 相關資源都會變爲不可用, 由於線程運行時間大多數不是立即運行的, 所以這種問題也比較常見.

本文講了三種線程同步方式 Joins, Mutexes和Condition Variables, 實際項目中後兩個用的非常多, join更多的用在最後釋放資源的時候用, 示例代碼都是非常簡單的基本使用方法, 還有很多高級用法沒有說明, 有興趣可自行查閱, 這裏推薦一個非常不錯的網站 http://www.yolinux.com/TUTORIALS/LinuxTutorialPosixThreads.html

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