什麼是執行緒:程序裡面的一條執行流程
為什麼要引入執行緒
這就不得說說程序的缺點了:
- 程序間的切換,會導致TLB、CPU的Cache失效
- 程序之間是隔離的,程序間的通訊需要打破隔離的壁障
而相較於程序而言,
-
執行緒的建立和銷燬是輕量級的。
-
同一程序的執行緒之間的切換,不會導致
TLB
失效、也不會導致CPUcache
失效. -
執行緒之間共享程序的所有資源,所以執行緒之間通訊的代價小
獲取程序的標識
程序:getpid()
, getppid()
執行緒:pthread_self()
NAME
pthread_self - obtain ID of the calling thread
SYNOPSIS
#include <pthread.h>
pthread_t pthread_self(void);
Compile and link with -pthread.
下面透過一個簡單的例子來了解一下pthread_self()
怎麼使用。
在使用之前,我們需要先知道pthread_t
是什麼型別,可以透過下面的命令獲取
gcc -E pthread_self.c | grep -nE "pthread_t"
1305:typedef unsigned long int pthread_t;
可以看到pthread_t
是unsigned long
型別
int main(int argc, char* argv[])
{
printf("pid = %d, ppid = %d\n", getpid(), getppid());
pthread_t tid = pthread_self();
printf("tid = %lu\n", tid);
return 0;
}
需要注意的是,為保證可移植性在編譯時需要再Makefile
檔案中加入-pthread
執行緒的基本操作
- 執行緒的建立
- 執行緒的終止
- 執行緒的等待
- 執行緒的清理
- 執行緒的遊離
在正式介紹pthead
系列函式時,需要了解一個pthread
的設計原則:
- 成功:返回0
- 失敗:返回錯誤碼,不會設定errno
執行緒的建立
在建立執行緒時,使用到pthread_creat()
函式。
NAME
pthread_create - create a new thread
SYNOPSIS
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.
thread
: 作為傳出引數,用於傳出建立新執行緒的id
,注意是指標型別。
attr
: 執行緒的屬性,傳入引數,一般填NULL,表示採用預設屬性
start_routine
: 執行緒的入口函式,型別和引數都是void*
型別,在C語言中指通用指標,可以傳遞或返回任意型別的值。
arg
:入口函式的引數,沒有引數傳NULL
透過一個簡單的例子來了解一下thread_create()
函式的使用
#include <func.h>
void printf_ids(char* prefix) {
printf("%s pid = %d, ppid = %d, thread_id = %lu\n",
prefix, getpid(), getppid(), pthread_self());
}
void* start_routine(void* arg){
printf_ids("new thread");
return NULL;
}
int main(int argc, char* argv[])
{
// 建立執行緒
pthread_t tid;
int err = pthread_create(&tid, NULL, start_routine, NULL);
if (err) {
error(1, err, "pthread_creat");
}
// 主執行緒
printf("main pthread\n");
return 0;
}
可以看到,主執行緒和子執行緒的執行順序是不確定的。
在第一種情況只列印了主執行緒的資訊,這是因為主執行緒在結束時,程序就會終止(所有子執行緒都會終止)。
這就不得不提一個線上程程式設計中的慣用法:主執行緒通常用於接收任務(或請求),然後將這些任務分配給其他子執行緒執行。主執行緒會等待所有子執行緒執行完畢後再結束,從而實現有序的退出。在實現執行緒的等待需要使用pthread_join()
函式,我們在後面介紹。
需要注意的是:主執行緒的執行流程是從main
函式開始,而子執行緒的執行流程從入口函式開始。
在這,提供一個技巧,在64位計算機中,如果傳遞的引數不超過8個位元組,可以將其分裝到一個指標中傳遞。下面是一個例項:
// pthread_create2.c
void printf_ids(char* prefix) {
printf("%s pid = %d, ppid = %d, thread_id = %lu\n",
prefix, getpid(), getppid(), pthread_self());
}
void* start_routine(void* arg){
int num = (int)arg;
printf_ids("new thread");
printf("num = %d\n", num);
return NULL;
}
int main(int argc, char* argv[])
{
// 建立執行緒
pthread_t tid;
int err = pthread_create(&tid, NULL, start_routine, (void*)4096);
if (err) {
error(1, err, "pthread_creat");
}
// 主執行緒
printf("main pthread\n");
sleep(2);
return 0;
}
當傳遞多個引數時,需要封裝到一個陣列(同型別)或結構體中。下面是一個簡單的例項,但會出現一些問題,我們先執行一下。
typedef struct {
int a;
double b;
char* message;
} Paras;
void printf_ids(char* prefix) {
printf("%s pid = %d, ppid = %d, thread_id = %lu\n",
prefix, getpid(), getppid(), pthread_self());
}
void* start_routine(void* arg){
Paras* arguments = (Paras*)arg;
printf_ids("new thread");
printf("a = %d, b = %lf, message = %s\n",
arguments->a, arguments->b, arguments->message);
return NULL;
}
int main(int argc, char* argv[])
{
// 建立執行緒
pthread_t tid;
Paras arguments = {1, 3.14, "Hello"};
int err = pthread_create(&tid, NULL, start_routine, &arguments);
if (err) {
error(1, err, "pthread_creat");
}
// 主執行緒
printf("main pthread\n");
sleep(2);
return 0;
}
可以看到,子程序可以正常獲取arguments
的值。這是因為arguments
儲存在主執行緒的棧上,由於執行緒之間資源共享,因此子執行緒可以成功獲取到arguments
的值
但需要注意的是,不要輕易訪問其他執行緒的棧空間。因為當訪問的執行緒終止時,其對應的堆空間也會被釋放。
若要線上程之間共享資料,可以放到程序的堆空間或程序的程式碼段和資料段。
若要存放到堆空間,需確保堆空間有且只能被其中一個程序free
.
執行緒的等待
要實現等待某執行緒完成,需要呼叫pthread_join()
函式
SYNOPSIS
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
Compile and link with -pthread.
第一個引數:thread
指定要等待執行緒的tid
第二個引數retval
:是void**
型別,是傳出引數,接收返回值(void*
,任意型別),不想接收返回值置為NULL。
下面是一個簡單的例子
#include <func.h>
void* start_routine(void* arg) {
printf("new thread start\n");
sleep(3); // 讓子執行緒sleep(3),看主執行緒是否提前結束
printf("new thread end\n");
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, start_routine, NULL);
// 等待tid終止,如果tid沒有終止,主執行緒就會一直阻塞
pthread_join(tid, NULL);
return 0;
}
執行結果
new thread start
new thread end
可以發現,當強制讓子執行緒sleep(3)
時,主執行緒會一直等待子執行緒結束,否則會一直阻塞。
執行緒的終止
引起程序終止的事件有:從main
返回、呼叫exit()
、使用訊號量機制kill -SIGKILL pid
引起執行緒終止的事件有:
-
從
start_routine
返回 -
呼叫
pthread_exit()
、 -
呼叫
pthread_cancel()
,一個執行緒給另一個執行緒傳送取消請求,若響應則終止
pthread_exit
NAME
pthread_exit - terminate calling thread
SYNOPSIS
#include <pthread.h>
void pthread_exit(void *retval);
Compile and link with -pthread.
可以看到pthread_exit()
的引數是void*
型別,是一個傳出引數。通常是執行緒的退出狀態或其他一些有用的結果。在同一程序中的其他執行緒,可以使用pthread_join()
接收。
下面是一個分別是從start_routine
返回以及使用pthread_exit()
退出的簡單示例。
// 從start_routine返回
void* start_routine(void* arg){
printf("new thread start\n");
printf("new thread end\n");
return NULL;
}
int main(int argc, char* argv[])
{
// 建立執行緒
pthread_t tid;
int err = pthread_create(&tid, NULL, start_routine, NULL);
if (err) {
error(1, err, "pthread_creat");
}
// 主執行緒
printf("main pthread: create new ptherad\n");
sleep(3); // 等待子執行緒結束
return 0;
}
// 呼叫pthread_exit()
void* start_routine(void* arg){
printf("new thread start\n");
pthread_exit(1);
printf("new thread end\n");
//return NULL;
}
int main(int argc, char* argv[])
{
// 建立執行緒
pthread_t tid;
int err = pthread_create(&tid, NULL, start_routine, NULL);
if (err) {
error(1, err, "pthread_creat");
}
// 主執行緒
printf("main pthread: create new ptherad\n");
sleep(3); // 等待子執行緒結束
return 0;
}
透過對比可以發現當使用pthread_eixt()
顯示地退出執行緒時,呼叫即退出。而使用start_routine
返回,執行緒可以透過其他執行緒退出。
pthread_cancel
(瞭解)
SYNOPSIS
#include <pthread.h>
int pthread_cancel(pthread_t thread);
Compile and link with -pthread.
透過下面的例子來簡單瞭解一下這個函式
#include <func.h>
void* start_routine(void* arg) {
for(;;) {
}
return NULL;
}
int main(int argc, char* argv[])
{
pthread_t tid;
int err = pthread_create(&tid, NULL, start_routine, NULL);
if (err) {
error(1, err, "pthread_create");
}
// 主執行緒傳送取消請求給子執行緒
err = pthread_cancel(tid);
if (err) {
error(1, err, "pthread_cancel");
}
// 等待子執行緒終止
pthread_join(tid, NULL);
return 0;
}
在上面的例子中,由於子執行緒的入口函式沒有響應主執行緒的終止訊號,因此主執行緒會在pthread_join(tid, NULL)
處阻塞等待子執行緒結束。
但如果將子執行緒的入口函式修改成下面的程式碼,會結束子執行緒的執行。
void* start_routine(void* arg) {
for(;;) {
sleep(1);
}
return NULL;
}
這是由於sleep(1)
是一個取消點。透過閱讀pthread_cancel
的man手冊,可以看到是否響應,以及何時響應取決於執行緒的屬性。
The pthread_cancel() function sends a cancellation request to the thread thread.
Whether and when the target thread reacts to the cancellation request depends on two
attributes that are under the control of that thread: its cancelability state and
type.
- 是否響應:取消
state
- 何時響應:取消
type
修改者兩個屬性可以透過下面兩個函式進行。
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
其中oldstate
和oldtype
是傳出引數,用於返回舊的狀態和舊的取消型別。
取消stste
:的取值有下面兩個
PTHREAD_CANCEL_ENABLE (預設) 響應取消狀態
PTHREAD_CANCEL_DISABLE 不響應取消狀態
取消type
: 的取值有:
PTHREAD_CANCEL_DEFERRED (預設) 在取消點響應
PTHREAD_CANCEL_ASYNCHRONOUS 可以在任意點響應
取消點可以檢視man手冊man 7 pthread
執行緒的清理
要實現執行緒的清理,需要先透過下面兩個函式註冊執行緒清理函式
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);
第一個引數:要執行的清理函式
第二個引數:傳遞給清理函式的引數
void pthread_cleanup_pop(int execute); 移除最近新增的清理處理程式。如果它的引數是非零值,則它還會執行清理處理程式
execute:是一個標誌,用於指示是否執行清理函式。
0: 不執行
1: 執行
透過一個簡單例子,來了解一些執行緒的清理
void cleanup(void* arg){
char* msg = (char*)arg;
puts(msg);
}
void* start_routine(void* arg) {
// 註冊執行緒清理函式
pthread_cleanup_push(cleanup, "111");
pthread_cleanup_push(cleanup, "222");
// 2. 執行執行緒邏輯
printf("new thread push\n");
sleep(1);
printf("new thread pop\n");
// 3. 執行緒退出
//pthread_exit(NULL);
//return NULL;
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
}
int main(int argc, char* argv[])
{
pthread_t tid;
int err = pthread_create(&tid, NULL, start_routine, NULL);
// 主執行緒傳送取消請求給子執行緒
err = pthread_cancel(tid);
// 等待子執行緒終止
pthread_join(tid, NULL);
return 0;
}
使用pthread_exit()
退出子執行緒的執行結果
new thread push
new thread pop
222
111
使用return NULL
退出子執行緒的執行結果
new thread push
new thread pop
使用pthread_cancel()
在退出點退出子執行緒的執行結果
new thread push
222
111
什麼時候執行執行緒清理函式呢?透過上面的例子不難發現:
- 使用
pthread_exit
退出 - 響應取消請求時,都會執行執行緒清理函式
注意:
- clear_push 和 clearup_pop一定要成對出現
- 從start_routine 返回不會執行執行緒清理函式
- 呼叫pthread_clearnup_pop(1)時,不會造成執行緒終止
執行緒的遊離
執行緒的遊離是指:斷開執行緒之間的attached
,使執行緒處於遊離狀態。
#include <pthread.h>
int pthread_detach(pthread_t thread);
typedef struct{
int id;
char name[20];
char gender;
} Student;
void print_stu_info(Student* s){
printf("%d %s %c",
s->id,
s->name,
s->gender);
}
void* start_routine(void* arg) {
printf("new thread start\n");
printf("new thread end\n");
Student* s = (Student*)malloc(sizeof(Student));
s->id = 1;
strcpy(s->name, "hello");
s->gender = 'f';
pthread_exit(s);
}
int main(int argc, char* argv[])
{
pthread_t tid;
int err = pthread_create(&tid, NULL, start_routine, NULL);
if (err) {
error(1, err, "pthread_create");
}
// detach 執行緒
err = pthread_detach(tid);
if (err) {
error(1, err, "pthread_detach");
}
Student* retval;
// 等待子執行緒終止
err = pthread_join(tid, (void**)&retval);
if (err) {
error(1, err, "pthread_join");
}
// 列印retval
print_stu_info(retval);
free(retval);
return 0;
}
在列印之前已經遊離了執行緒,因此會join失敗。
./pthread_detach: pthread_join: Invalid argument
同步、非同步、併發、並行
程式的執行方式:非同步、同步、併發、並行
程式設計正規化:貫通式,物件導向、函式式、範型
非同步:任務之間相互獨立,不需要等待前一個任務完成就可以開始執行下一個任務,非同步模式下事件的執行順序一般是隨機的,一個任務的執行不會阻塞其他任務的進行。
同步:時間之間的執行順序是確定的,每一個任務需要等待前一個任務完成才可以開始執行。可以看作它們共同遵循一定的規則,可以讓程式有秩序的執行。同步的基礎是通訊,通訊的基礎是共享資源。
併發:併發是一種現象。兩個執行流程在一段時間內可以交替執行。
並行:是一種技術。指的是在同一個時間點可以執行多個任務。
由於執行緒間的非同步執行,從而導致竟態條件的產生。
竟態條件
竟態條件是指:有多個執行流程同時訪問共享資源,從而導致執行的結果由執行流程訪問共享資源的先後順序決定。
臨界區
為避免竟態條件的額產生,提出了臨界區的概念。
臨界區:訪問共享資源的一段程式碼,資源通常是一個變數或資料結構
鎖
為了實現併發程式設計,我們希望原子式執行一系列指令,但由於單處理器上的中斷(或多個執行緒在多處理器上併發執行),很難實現。
因此,鎖(lock)直接解決這個問題。
什麼是鎖
鎖是一個變數,因此需要宣告一個某種型別的鎖變數(lock variable)才能使用。
鎖變數儲存了鎖在某一時刻的狀態。
透過給臨界區加鎖,可以保證臨界區內只有一個執行緒活躍,從而保證對臨界區的訪問是原子的。鎖將原本由作業系統排程的混亂狀態變為可控。
互斥鎖
SYNOPSIS
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
lock
: 上鎖。先嚐試獲取鎖,若鎖被佔有,會一直阻塞,直到獲取鎖
unlock
: 釋放鎖
trylock
: 嘗試上鎖。嘗試獲取鎖,獲取不成功立即返回。
初始化鎖
在使用鎖之前,需要正確初始化鎖。
SYNOPSIS
#include <pthread.h>
// 銷燬鎖
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 動態初始化鎖
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// 靜態初始化鎖
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
動態初始化鎖
mutex
: 指向將要被初始化的互斥鎖變數的指標attr
: 可選屬性。如果設定為NULL
,初始化為預設屬性
需要注意的是,若採用動態初始化,需要使用pthread_mutex_destory()
銷燬鎖
銷燬鎖
int pthread_mutex_destroy(pthread_mutex_t *mutex);
怎樣上鎖
一個上鎖的簡單步驟就是:
- 需要先判斷出臨界區
- 在臨界區前上鎖
- 臨界區後釋放鎖
為保證程式的邏輯,
下面是一個使用使用靜態初始化方式上鎖的例子:
請完善下面程式:
int main(void) {
long long* value = (long long*) calloc(1, sizeof(long long));
// 建立兩個執行緒
// 第一個執行緒執行 (*value)++ 10000000次
// 第二個執行緒葉執行 (*value)++ 10000000次
// 主線乘等待兩個子執行緒結束。並列印 *value 的值。
}
// 靜態初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 執行緒函式
void* start_routine(void* arg) {
long long* value = (long long*)arg;
for (int i = 0; i < 10000000; i++) {
// 上鎖
pthread_mutex_lock(&mutex);
(*value)++;
// 釋放鎖
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(void) {
long long* value = (long long*) calloc(1, sizeof(long long));
// 建立兩個執行緒
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, start_routine, value);
pthread_create(&tid2, NULL, start_routine, value);
// 主線乘等待兩個子執行緒結束。並列印 *value 的值。
err = pthread_join(tid1, NULL);
err = pthread_join(tid2, NULL);
// 輸出最終結果。
printf("*value = %lld\n", *value);
free(value);
}
執行結果如下:
*value=2000000
如果不上鎖呢?
多次執行:
*value = 11256461
*value = 11863935
*value = 11400809
可以發現,每次執行結果都不同,發生的原因是什麼呢?
檢視一下執行+1操作的彙編程式碼:
mov 0x8049a1c, %eax
add $0x1, %eax
mov %eax, 0x8049a1c
假設兩個執行緒在執行時出現下面這種情況,導致競爭狀態的產生,因此需要上鎖。
下面,我們以銀行的為例,來詳細介紹一下鎖的使用
鎖的使用
實現銀行取錢功能
typedef struct {
int id;
int balance;
} Account;
Account acct1 = {1, 100};
pthread_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 取錢
int withdraw(Account* acct, int money) {
pthread_mutex_lock(&mutex);
// 檢驗
if (acct->balance < money) {
return 0;
}
// 取錢
acct->balance -= money;
pthread_mutex_unlock(&mutex);
return money;
}
void* start_routine(void* arg) {
int money = (int)arg;
int n = withdraw(&acct1, money);
printf("%lu withdraw $%d\n", pthread_self(), n);
return NULL;
}
void* start_routine1(void* arg) {
int money = (int)arg;
int n = withdraw(&acct1, money);
printf("%lu withdraw $%d\n", pthread_self(), n);
return NULL;
}
int main(int argc, char* argv[])
{
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, start_routine, (void*)100);
pthread_create(&tid2, NULL, start_routine1, (void*)100);
// 主執行緒等待子執行緒
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
這樣就可以使取錢操作正確進行。
但存在一個問題:只有一把鎖,因此在同一時刻只能有一個人可以取錢,導致併發量很低,在實際環境中很不適用。
在實際環境中,應該是每個使用者都有自己的鎖,自己取錢時不影響其他人。可以修改程式碼
typedef struct {
int id;
int balance;
pthread_mutex_t mutex;
} Account;
Account acct1 = {1, 100};
//pthread_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 取錢
int withdraw(Account* acct, int money) {
pthread_mutex_lock(&acct->mutex);
// 檢驗
if (acct->balance < money) {
pthread_mutex_unlock(&acct->mutex);
return 0;
}
// 取錢
acct->balance -= money;
pthread_mutex_unlock(&acct->mutex);
return money;
}
下面實現一個簡單的轉賬功能。
// 轉賬
int transfer(Account* acctA, Account* acctB, int money) {
pthread_mutex_lock(&acctA->mutex);
pthread_mutex_lock(&acctB->mutex);
if (acctA->balance < money) {
pthread_mutex_unlock(&acctA->mutex);
pthread_mutex_unlock(&acctB->mutex);
return 0;
}
acctA->balance -= money;
acctB->balance += money;
pthread_mutex_unlock(&acctA->mutex);
pthread_mutex_unlock(&acctB->mutex);
return money;
}
但如果線上程A執行期間強行sleep(1)則可能發生死鎖,
程式的執行流程如下,由於tid1會一直等待tid2釋放鎖,tid2也會一直等待tid1釋放鎖,所以程式處於死鎖狀態。而主執行緒則在join處等待子執行緒結束。
死鎖
死鎖(deadlock)是指多個程序或執行緒在執行過程中造成的一種相互等待的現象,若無外力干涉,將無法向前推進。
死鎖出現的原因:
- 互斥:至少有一個資源處於非共享模式。即,在一段時間內只有一個程序可以使用資源。如果另外一個程序請求該資源,請求者只能等待,直到資源被釋放。
- 持有並等待:一個程序至少佔有一個資源,並正在等待獲取被其他程序持有的資源。
- 不能搶佔:資源不能被搶佔。一旦資源被佔有,在它被使用完成並資源釋放之前,不能被強行奪取
- 迴圈等待:存在一條程序資源的迴圈鏈,鏈中的每一個程序至少佔有一個資源,該資源被鏈中的下一個程序鎖請求。
以上4個條件缺一不可。因此破除死鎖只需破壞其中一個條件既可。
破壞迴圈等待
如何破壞迴圈等待,最常用的方式是按照一定順序上鎖。修改轉賬程式碼如下。
// 轉賬
int transfer(Account* acctA, Account* acctB, int money) {
if (acctA->id < acctB->id) {
pthread_mutex_lock(&acctA->mutex);
sleep(1); // 切換
pthread_mutex_lock(&acctB->mutex);
} else {
pthread_mutex_lock(&acctB->mutex);
sleep(2); // 切換
pthread_mutex_lock(&acctA->mutex);
}
if (acctA->balance < money) {
pthread_mutex_unlock(&acctA->mutex);
pthread_mutex_unlock(&acctB->mutex);
return 0;
}
acctA->balance -= money;
acctB->balance += money;
pthread_mutex_unlock(&acctA->mutex);
pthread_mutex_unlock(&acctB->mutex);
return money;
}
破壞不能搶佔
// 破壞不能搶佔
start:
pthread_mutex_lock(&acctA->mutex);
sleep(1);
int err = pthread_mutex_trylock(&acctB->mutex);
if (err) {
pthread_mutex_unlock(&acctA->mutex);
// 停留一個隨機時間
int nsec = rand() % 5;
sleep(nsec);
goto start;
}
破壞持有並等待
破壞持有並等待,可以透過要麼一次獲得所有鎖,要麼一次也不獲取。
因此,可以定義一個全域性鎖,將所有獲取鎖的操作變為原子操作
// 2. 持有並等待
pthread_mutex_lock(&protection);
pthread_mutex_lock(&acctA->mutex);
sleep(1); // 切換
pthread_mutex_lock(&acctB->mutex);
pthread_mutex_unlock(&protection);
破壞互斥
破壞互斥,需要硬體的支援。
條件變數
鎖並不是併發程式設計所需的唯一原語。詳細來說,在很多情況下,執行緒需要檢查某一條件滿足之後,才會繼續執行。
如何等待一個條件滿足呢?
簡單的方法是自旋直到條件滿足,下面是一個簡單例子。
volatile int done = 0;
void* start_continue(void* arg) {
printf("child\n");
done = 1;
return NULL;
}
int main(int argc, char* argv[]) {
printf("parent begin\n");
pthread_t tid;
pthread_create(&tid, NULL, child, NULL); // create child
while (done == 0)
; // 自旋
printf("parent end\n");
return 0;
}
執行緒也可以使用條件變數來等待一個條件變為真。使用條件變數,需要等待和喚醒機制。
int pthread_cond_broadcast(pthread_cond_t *cond); // 等待
int pthread_cond_signal(pthread_cond_t *cond); // 喚醒
在使用條件變數時,必須有另外一個與此條件相關的鎖,在使用pthread_cond_wait()
或pthread_cond_signal()
函式時,必須擁有該鎖。
典型的用法如下:
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_lock(&lock);
while (ready == 0) {
pthread_cond_wait(&cond, &lock);
}
pthread_mutex_unlock(&lock);
在初始化相關的鎖和條件之後,一個執行緒檢查變數 ready 是否準備好。如果沒有,那麼執行緒只是簡單地呼叫等待函式以便休眠,直到其他執行緒喚醒它。
喚醒執行緒的程式碼可以執行在另外某個執行緒中
Pthread_mutex_lock(&lock);
ready = 1;
Pthread_cond_signal(&cond);
Pthread_mutex_unlock(&lock);
等待呼叫將鎖(互斥鎖)作為其第二個引數,而訊號呼叫僅需要一個條件。
這是因為,等待呼叫除了使呼叫執行緒進入休眠狀態外,還會讓呼叫者在睡眠時釋放鎖。如果不這樣,其他執行緒就不會獲得鎖將其喚醒。
但是,在被喚醒之後返回之前,pthread_cond_wait()會重新獲取該鎖
條件變數的初始化和銷燬
在使用條件變數之前需要對其進行初始化
SYNOPSIS
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
// 動態初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
// 靜態初始胡
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
通知機制(條件滿足)
當條件滿足時,通知等待該條件成立的執行緒
int pthread_cond_signal(pthread_cond_t *cond);
當cond
滿足時,會喚醒一個等待該條件的執行緒。
主要注意的是:核心在實現時,為了效能考慮,可能會喚醒多個等待的執行緒
int pthread_cond_broadcast(pthread_cond_t *cond);
會喚醒所有等待該條件的執行緒。
等待機制(條件不滿足)
條件一直不成立,就會一直等待
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
pthread_cond_t *restrict cond
: 指向條件變數的指標。
pthread_mutex_t *restrict mutex
: 互斥鎖
生產者/消費者(有界緩衝區)問題
假設一個或多個生產者執行緒和一個或多個消費者執行緒。生產者將生產的資料項放入緩衝區;消費者從緩衝區中取走資料項,以某種方式消費。
因為緩衝區是共享資源,所以必須透過同步機制來進行訪問。以免產生竟態條件。
cond_t cond;
mutex_t mutex;
void *producer(void *arg) {
int i;
for (i = 0; i < loops; i++) {
Pthread_mutex_lock(&mutex); // p1
if (count == 1) // p2
Pthread_cond_wait(&cond, &mutex); // p3
put(i); // p4
Pthread_cond_signal(&cond); // p5
Pthread_mutex_unlock(&mutex); // p6
}
}
void *consumer(void *arg) {
int i;
for (i = 0; i < loops; i++) {
Pthread_mutex_lock(&mutex); // c1
if (count == 0) // c2
Pthread_cond_wait(&cond, &mutex); // c3
int tmp = get(); // c4
Pthread_cond_signal(&cond); // c5
Pthread_mutex_unlock(&mutex); // c6
printf("%d\n", tmp);
}
}