雖然本身是做Android開發(fā)的, 但經(jīng)常會(huì)用到C/C++, 最近項(xiàng)目中剛好通過線程同步解決了一個(gè)問題,線程知識(shí)應(yīng)用太廣泛了, 所以在此記錄下關(guān)于C/C++中比較實(shí)用基礎(chǔ)知識(shí), 本篇文章就說明一下pthread中線程同步的幾種方式.
pthread
pthread 即 POSIX threads, POSIX表示可移植操作系統(tǒng)接口(Portable Operating System Interface of UNIX), 所以pthread就是在這個(gè)標(biāo)準(zhǔn)下實(shí)現(xiàn)的線程, 廣泛用于類Unix操作系統(tǒng)上, 使用需要引入的頭文件為 #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創(chuàng)建很簡單, 調(diào)用pthread_create()
即可, 函數(shù)定義如下:
int pthread_create(pthread_t * thread,
const pthread_attr_t * attr,
void * (*start_routine)(void *),
void *arg);
參數(shù)1: 存儲(chǔ)創(chuàng)建線程的id
參數(shù)2:一些線程屬性, 如果只是普通使用, 傳NULL
參數(shù)3: 函數(shù)指針, 即你要在此線程中運(yùn)行的函數(shù)
參數(shù)4:用于傳遞參數(shù)到運(yùn)行的函數(shù)中
注意:如果你是C/C++混合編程, 第三個(gè)參數(shù)在C++中只能傳遞全局函數(shù)或者類的靜態(tài)函數(shù), 不能傳類的普通成員函數(shù), 因?yàn)槠胀惓蓡T函數(shù)是屬于對(duì)象的, 類沒有實(shí)例化是不能使用這個(gè)函數(shù)的.
pthread_create()
創(chuàng)建線程后, 線程會(huì)立即運(yùn)行, 通過調(diào)用pthread_join()
等待線程結(jié)束, 此函數(shù)會(huì)阻塞當(dāng)前線程, pthread_join()
成功返回后, 線程資源就會(huì)被釋放, 上面的示例代碼,編譯(編譯要加-pthread參數(shù))運(yùn)行后輸出結(jié)果是不確定的, 原因是多個(gè)線程沒有同步, 造成一些不可預(yù)料的結(jié)果發(fā)生, 其中某次輸出結(jié)果如下:
$ ./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()
大多數(shù)情況下都是用來結(jié)束線程的, 即退出程序釋放相關(guān)線程, 但也可以作為簡單同步功能來使用, 比如將上面示例代碼修改為如下方式:
#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);
}
//前一個(gè)線程結(jié)束后才運(yùn)行下一個(gè)線程
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);
}
這樣修改后每次結(jié)果都是確定的, 原因是我們等前一個(gè)線程運(yùn)行完成后,才啟動(dòng)下一個(gè)線程, 之前是一次性啟動(dòng), 運(yùn)行結(jié)果每次都是:
$ ./test_join
Thread 0 value:1
Thread 1 value:2
Thread 2 value:3
Thread 3 value:4
Thread 4 value:5
此方式在時(shí)間項(xiàng)目中使用場(chǎng)景有限, 很少使用.
線程同步-Mutexes
Mutex即互斥量, 如果上鎖后, 其他線程則無法獲得鎖導(dǎo)致線程阻塞, 直到鎖被釋放,才能再次獲得鎖進(jìn)而執(zhí)行相關(guān)代碼, 示例代碼:
#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;
- 初始化有兩種方式, 調(diào)用
pthread_mutex_init(&mLock, NULL);
或者mLock = PTHREAD_MUTEX_INITIALIZER;
效果一樣, 后者是通過定義的宏來實(shí)現(xiàn)的. - 調(diào)用lock和unlock函數(shù), 上鎖(獲得鎖)
pthread_mutex_lock(&mLock);
, 解鎖(釋放鎖)pthread_mutex_unlock(&mLock);
, 同一時(shí)間只有一個(gè)線程能獲得該鎖, 被lock后, 其他線程調(diào)用lock函數(shù)會(huì)阻塞當(dāng)前線程. - 釋放定義的pthread_mutex_t 資源, 調(diào)用
pthread_mutex_destroy(&mLock);
上面代碼運(yùn)行結(jié)果如下:
$ ./test_mutex
Thread 0 value:1
Thread 1 value:2
Thread 2 value:3
Thread 3 value:4
Thread 4 value:5
注意: 上面的例子只能保證value的值是按照預(yù)期從1變?yōu)?的, 但并不能保證thread的運(yùn)行順序,也就是說你運(yùn)行的結(jié)果有可能是下面這樣的:
$ ./test_mutex
Thread 0 value:1
Thread 2 value:2
Thread 1 value:3
Thread 3 value:4
Thread 4 value:5
原因是上面的例子并不能保證各個(gè)線程獲取鎖的順序, 因?yàn)槊總€(gè)線程獲得鎖的優(yōu)先級(jí)是相同的, 所以順序有可能每次都不一樣.
線程同步-Condition Variables
Condition Variables 即條件變量, 在線程同步使用過程中要配合上面的mutex進(jìn)行使用, 實(shí)際多線程開發(fā)使用較多, 比如經(jīng)典的生產(chǎn)者-消費(fèi)者關(guān)系就可以通過Condition Variables來實(shí)現(xiàn), 常用的場(chǎng)景為滿足某個(gè)條件讓當(dāng)前線程阻塞進(jìn)行等待, 當(dāng)其他線程滿足另一個(gè)條件后, 通知正在等待的線程進(jìn)行工作.
下面通過一個(gè)簡單例子來說明下基本使用:
#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);
//喚醒一個(gè)線程,如果當(dāng)前有多個(gè)線程等待,
//根據(jù)優(yōu)先級(jí)和等待時(shí)間選擇其中一個(gè)線程進(jìn)行喚醒
//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;
- 讓當(dāng)前線程阻塞進(jìn)入等待狀態(tài)
pthread_mutex_lock(&mLock);
pthread_cond_wait(&mCond, &mLock);
pthread_mutex_unlock(&mLock);
注意, 此處必須和mutex一起使用, 即調(diào)用pthread_cond_wait()
這個(gè)函數(shù)本身要上鎖, 否則會(huì)產(chǎn)生不可預(yù)料異常.
- 喚醒等待此條件變量的線程
pthread_mutex_lock(&mLock);
//喚醒所有等待mCond的線程
pthread_cond_broadcast(&mCond);
//喚醒一個(gè)線程,如果當(dāng)前有多個(gè)線程等待,
//根據(jù)優(yōu)先級(jí)和等待時(shí)間選擇其中一個(gè)線程進(jìn)行喚醒
//pthread_cond_signal(&mCond);
pthread_mutex_unlock(&mLock);
- 釋放資源
pthread_cond_destroy(&mCond);
上面示例代碼基本邏輯是啟動(dòng)五個(gè)線程, 默認(rèn)開始就阻塞(等待mCond), 然后主線程sleep 1s后, 喚醒所有等待的線程, 此時(shí)5個(gè)線程會(huì)同時(shí)運(yùn)行同一個(gè)函數(shù), 輸出結(jié)果不可預(yù)料, 某次結(jié)果如下:
$ ./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
總結(jié)
根據(jù)我自己遇到的一些多線程問題, 我覺得多線程開發(fā)需要注意一下幾點(diǎn):
- 不要以常規(guī)思維思考沒做過同步的一些代碼的運(yùn)行結(jié)果, 很多結(jié)果你自己是沒法預(yù)料的.
- 寫代碼時(shí)思路要清晰, 有l(wèi)ock就要保證能在合適時(shí)機(jī)unlock, 不然很容易出現(xiàn)死鎖.
- 線程參數(shù)傳遞大多數(shù)是通過指針來實(shí)現(xiàn)的, 需要注意這些參數(shù)的生命周期, C/C++和Java不同,
Java中只要有引用對(duì)象就不會(huì)被釋放, 但C/C++中則不同, 超出作用域或者手動(dòng)釋放, 相關(guān)資源都會(huì)變?yōu)椴豢捎? 由于線程運(yùn)行時(shí)間大多數(shù)不是立即運(yùn)行的, 所以這種問題也比較常見.
本文講了三種線程同步方式 Joins, Mutexes和Condition Variables, 實(shí)際項(xiàng)目中后兩個(gè)用的非常多, join更多的用在最后釋放資源的時(shí)候用, 示例代碼都是非常簡單的基本使用方法, 還有很多高級(jí)用法沒有說明, 有興趣可自行查閱, 這里推薦一個(gè)非常不錯(cuò)的網(wǎng)站 http://www.yolinux.com/TUTORIALS/LinuxTutorialPosixThreads.html