雖然本身是做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;
- 初始化有兩種方式, 調用
pthread_mutex_init(&mLock, NULL);
或者mLock = PTHREAD_MUTEX_INITIALIZER;
效果一樣, 後者是通過定義的宏來實現的. - 調用lock和unlock函數, 上鎖(獲得鎖)
pthread_mutex_lock(&mLock);
, 解鎖(釋放鎖)pthread_mutex_unlock(&mLock);
, 同一時間只有一個線程能獲得該鎖, 被lock後, 其他線程調用lock函數會阻塞當前線程. - 釋放定義的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差不多,如下:
- 定義 pthread_cond_t
pthread_cond_t mCond;
- 初始化也和mutex一樣兩種方式
pthread_cond_init(&mCond, NULL);
和mCond = PTHREAD_COND_INITIALIZER;
- 讓當前線程阻塞進入等待狀態
pthread_mutex_lock(&mLock);
pthread_cond_wait(&mCond, &mLock);
pthread_mutex_unlock(&mLock);
注意, 此處必須和mutex一起使用, 即調用pthread_cond_wait()
這個函數本身要上鎖, 否則會產生不可預料異常.
- 喚醒等待此條件變量的線程
pthread_mutex_lock(&mLock);
//喚醒所有等待mCond的線程
pthread_cond_broadcast(&mCond);
//喚醒一個線程,如果當前有多個線程等待,
//根據優先級和等待時間選擇其中一個線程進行喚醒
//pthread_cond_signal(&mCond);
pthread_mutex_unlock(&mLock);
- 釋放資源
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
總結
根據我自己遇到的一些多線程問題, 我覺得多線程開發需要注意一下幾點:
- 不要以常規思維思考沒做過同步的一些代碼的運行結果, 很多結果你自己是沒法預料的.
- 寫代碼時思路要清晰, 有lock就要保證能在合適時機unlock, 不然很容易出現死鎖.
- 線程參數傳遞大多數是通過指針來實現的, 需要注意這些參數的生命週期, C/C++和Java不同,
Java中只要有引用對象就不會被釋放, 但C/C++中則不同, 超出作用域或者手動釋放, 相關資源都會變爲不可用, 由於線程運行時間大多數不是立即運行的, 所以這種問題也比較常見.
本文講了三種線程同步方式 Joins, Mutexes和Condition Variables, 實際項目中後兩個用的非常多, join更多的用在最後釋放資源的時候用, 示例代碼都是非常簡單的基本使用方法, 還有很多高級用法沒有說明, 有興趣可自行查閱, 這裏推薦一個非常不錯的網站 http://www.yolinux.com/TUTORIALS/LinuxTutorialPosixThreads.html