1.2 什麼是執行緒
1.3 程式,行程,執行緒
1.4 多工與多執行緒
2 第 35 章 執行緒
http://learn.akae.cn/media/ch35.html
2.1 1. 執行緒的概念
我們知道,行程(Process)在各自獨立的位址空間中運行,行程(Process)之間共用資料需要用mmap或者行程(Process)間通信機制,本節我們學習如何在一個行程(Process)的位址空間中執行多個執行緒。有些情況需要在一個行程(Process)中同時執行多個控制流程,這時候執行緒就派上了用場,比如實現一個圖形介面的下載軟體,一方面需要和使用者交互,等待和處理使用者的滑鼠鍵盤事件,另一方面又需要同時下載多個檔,等待和處理從多個網路主機發來的資料,這些任務都需要一個“等待-處理”的迴圈,可以用多執行緒實現,一個執行緒專門負責與用戶交互,另外幾個執行緒每個執行緒負責和一個網路主機通信。
以前我們講過,main函數和信號處理函數是同一個行程(Process)位址空間中的多個控制流程,多執行緒也是如此,但是比信號處理函數更加靈活,信號處理函數的控制流程只是在信號遞達時產生,在處理完信號之後就結束,而多執行緒的控制流程可以長期並存,作業系統會在各執行緒之間調度和切換,就像在多個行程(Process)之間調度和切換一樣。由於同一行程(Process)的多個執行緒共用同一位址空間,因此Text Segment、Data Segment都是共用的,如果定義一個函數,在各執行緒中都可以調用,如果定義一個全域變數,在各執行緒中都可以訪問到,除此之外,各執行緒還共用以下行程(Process)資源和環境:
l 檔描述符表
l 每種信號的處理方式(SIG_IGN、SIG_DFL或者自訂的信號處理函數)
l 當前工作目錄
l 用戶id和組id
但有些資源是每個執行緒各有一份的:
l 執行緒id
l 上下文,包括各種暫存器的值、程式計數器和堆疊/棧(stack)指標
l 堆疊/棧(stack)空間
l errno變數
l 信號遮罩字
l 調度優先順序
我們將要學習的執行緒庫函數是由POSIX標準定義的,稱為POSIX thread或者pthread。
在Linux上執行緒函數位於libpthread共用庫中,因此在編譯時要加上 -lpthread選項。
2.2 2. 執行緒控制
2.2.1 2.1. 創建執行緒
#include <pthread.h>
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void*),
void *restrict arg);
ex: pthread_create(&ntid, NULL, thr_fn, "new thread: ");
返回值:成功返回0,失敗返回錯誤號。
以前學過的系統函數都是成功返回0,失敗返回-1,而錯誤號保存在全域變數errno中,而pthread庫的函數都是通過返回值返回錯誤號,雖然每個執行緒也都有一個errno,但這是為了相容其它函數介面而提供的,pthread庫本身並不使用它,通過返回值返回錯誤碼更加清晰。
在一個執行緒中調用pthread_create()創建新的執行緒後,當前執行緒從pthread_create()返回繼續往下執行,而新的執行緒所執行的代碼由我們傳給pthread_create的函數指標start_routine決定。
start_routine函數接收一個參數,是通過pthread_create的arg參數傳遞給它的,該參數的類型為void *,這個指標按什麼類型解釋由調用者自己定義。start_routine的返回數值型別也是void *,這個指標的含義同樣由調用者自己定義。start_routine返回時,這個執行緒就退出了,其它執行緒可以調用pthread_join得到start_routine的返回值,類似于父進程調用wait(2)得到子進程的退出狀態,稍後詳細介紹pthread_join。
pthread_create成功返回後,新創建的執行緒的id被填寫到thread參數所指向的記憶體單元。我們知道行程(Process) id的類型是pid_t,每個行程(Process)的id在整個系統中是唯一的,調用getpid(2)可以獲得當前進程的id,是一個正整數值。
執行緒id的類型是thread_t,它只在當前行程(Process)中保證是唯一的,在不同的系統中thread_t這個類型有不同的實現,它可能是一個整數值,也可能是一個結構體,也可能是一個位址,所以不能簡單地當成整數用printf列印,調用pthread_self(3)可以獲得當前執行緒的id。
attr參數表示執行緒屬性,本章不深入討論執行緒屬性,所有代碼例子都傳NULL給attr參數,表示執行緒屬性取缺省值,感興趣的讀者可以參考[APUE2e]。首先看一個簡單的例子:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
pthread_t ntid;
void printids(const char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid,
(unsigned int)tid, (unsigned int)tid);
}
void *thr_fn(void *arg)
{
printids(arg);
return NULL;
}
int main(void)
{
int err;
err = pthread_create(&ntid, NULL, thr_fn, "new thread: ");
if (err != 0) {
fprintf(stderr, "can't create thread: %s\n", strerror(err));
exit(1);
}
printids("main thread:");
sleep(1);
return 0;
}
編譯運行結果如下:
$ gcc main.c -lpthread
$ ./a.out
main thread: pid 7398 tid 3084450496 (0xb7d8fac0)
new thread: pid 7398 tid 3084446608 (0xb7d8eb90)
可知在Linux上,thread_t類型是一個位址值,屬於同一行程(Process)的多個執行緒調用getpid(2)可以得到相同的行程號,而調用pthread_self(3)得到的執行緒號各不相同。
由於pthread_create的錯誤碼不保存在errno中,因此不能直接用perror(3)列印錯誤資訊,可以先用strerror(3)把錯誤碼轉換成錯誤資訊再列印。
如果任意一個執行緒調用了exit或_exit,則整個行程的所有執行緒都終止,由於從main函數return也相當於調用exit,為了防止新創建的執行緒還沒有得到執行就終止,我們在main函數return之前延時1秒,這只是一種權宜之計,即使主執行緒等待1秒,內核也不一定會調度新創建的執行緒執行,下一節我們會看到更好的辦法。
???思考題:主執行緒在一個全域變數ntid中保存了新創建的執行緒的id,如果新創建的執行緒不調用pthread_self而是直接列印這個ntid,能不能達到同樣的效果?
2.2.2 2.2. 終止執行緒
如果需要只終止某個執行緒而不終止整個行程,可以有三種方法:
· 從執行緒函數return。這種方法對主執行緒不適用,從main函數return相當於調用exit。
· 一個執行緒可以調用pthread_cancel終止同一行程中的另一個執行緒。
· 執行緒可以調用pthread_exit終止自己。
用pthread_cancel終止一個執行緒分同步和非同步兩種情況,比較複雜,本章不打算詳細介紹,讀者可以參考[APUE2e]。
下麵介紹pthread_exit的和pthread_join的用法。
#include <pthread.h>
void pthread_exit(void *value_ptr);
value_ptr是void *類型,和執行緒函數返回值的用法一樣,其它執行緒可以調用pthread_join獲得這個指標。
需要注意,pthread_exit或者return返回的指標所指向的記憶體單元必須是全域的或者是用malloc分配的,不能在執行緒函數的堆疊/棧(stack)上分配,因為當其它執行緒得到這個返回指標時執行緒函數已經退出了。
#include <pthread.h>
int pthread_join(pthread_t thread, void **value_ptr);
功能:等待指定的執行緒結束。
返回值:成功返回0,失敗返回錯誤號
調用該函數的執行緒將掛起等待,直到id為thread的執行緒終止。
thread執行緒以不同的方法終止,通過pthread_join得到的終止狀態是不同的,總結如下:
· 如果thread執行緒通過return返回,value_ptr所指向的單元裡存放的是thread執行緒函數的返回值。
· 如果thread執行緒被別的執行緒調用pthread_cancel異常終止掉,value_ptr所指向的單元裡存放的是常數PTHREAD_CANCELED。
· 如果thread執行緒是自己調用pthread_exit終止的,value_ptr所指向的單元存放的是傳給pthread_exit的參數。
如果對thread執行緒的終止狀態不感興趣,可以傳NULL給value_ptr參數。
看下面的例子(省略了出錯處理):
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *thr_fn1(void *arg)
{
printf("thread 1 returning\n");
return (void *)1;
}
void *thr_fn2(void *arg)
{
printf("thread 2 exiting\n");
pthread_exit((void *)2);
}
void *thr_fn3(void *arg)
{
while(1) {
printf("thread 3 writing\n");
sleep(1);
}
}
int main(void)
{
pthread_t tid;
void *tret;
pthread_create(&tid, NULL, thr_fn1, NULL);//for return
pthread_join(tid, &tret);
printf("thread 1 exit code %d\n", (int)tret);
pthread_create(&tid, NULL, thr_fn2, NULL);// pthread_exit
pthread_join(tid, &tret);
printf("thread 2 exit code %d\n", (int)tret);
pthread_create(&tid, NULL, thr_fn3, NULL);//for while(1)
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &tret);
printf("thread 3 exit code %d\n", (int)tret);
return 0;
}
運行結果是:
$ ./a.out
thread 1 returning
thread 1 exit code 1
thread 2 exiting
thread 2 exit code 2
thread 3 writing
thread 3 writing
thread 3 writing
thread 3 exit code -1
$ ./a.out (無pthread_join時)
thread 1 exit code -1077688820
thread 2 exit code -1077688820
pid 3086134160 tid 3086134160 (0x3d0f00)
thread 1 returning
pid 3075644304 tid 3075644304 (0x0)
thread 2 exiting
pid 3065154448 tid 3065154448 (0x0)
thread 3 writing
thread 3 writing
thread 3 writing
thread 3 exit code -1077688820
可見在Linux的pthread庫中常數PTHREAD_CANCELED的值是-1。可以在標頭檔pthread.h中找到它的定義:
#define PTHREAD_CANCELED ((void *) -1)
一般情況下,執行緒終止後,其終止狀態一直保留到其它執行緒調用pthread_join獲取它的狀態為止。
但是執行緒也可以被置為detach狀態,這樣的執行緒一旦終止就立刻回收它佔用的所有資源,而不保留終止狀態。不能對一個已經處於detach狀態的執行緒調用pthread_join,這樣的調用將返回EINVAL。對一個尚未detach的執行緒調用pthread_join或pthread_detach都可以把該執行緒置為detach狀態,也就是說,不能對同一執行緒調用兩次pthread_join,或者如果已經對一個執行緒調用了pthread_detach就不能再調用pthread_join了。
#include <pthread.h>
int pthread_detach(pthread_t tid);
返回值:成功返回0,失敗返回錯誤號。
2.3 3. 執行緒間同步
同步執行緒的方式: 封鎖呼叫(join)互斥鎖(mutex)條件變數(condition variable)。
Join語意是加入,但跟官網的解釋不太一樣,Thread.Join 方法在官網的解釋是:封鎖呼叫執行緒,直到執行緒結束為止。
2.3.1 3.1. mutex(互斥鎖)
多個執行緒同時訪問共用資料時可能會訪問衝突(Access Conflict),這跟前面講信號時所說的可重入性是同樣的問題。比如兩個執行緒都要把某個全域變數增加1,這個操作在某平臺需要三條指令完成:
1. 從記憶體讀變數值到暫存器
2. 暫存器的值加1
3. 將暫存器的值寫回記憶體
假設兩個執行緒在多處理器平臺上同時執行這三條指令,則可能導致下圖所示的結果,最後變數只加了一次而非兩次。
圖 35.1. 並行訪問衝突(Access Conflict)
http://learn.akae.cn/media/ch35.html
2.1 1. 執行緒的概念
我們知道,行程(Process)在各自獨立的位址空間中運行,行程(Process)之間共用資料需要用mmap或者行程(Process)間通信機制,本節我們學習如何在一個行程(Process)的位址空間中執行多個執行緒。有些情況需要在一個行程(Process)中同時執行多個控制流程,這時候執行緒就派上了用場,比如實現一個圖形介面的下載軟體,一方面需要和使用者交互,等待和處理使用者的滑鼠鍵盤事件,另一方面又需要同時下載多個檔,等待和處理從多個網路主機發來的資料,這些任務都需要一個“等待-處理”的迴圈,可以用多執行緒實現,一個執行緒專門負責與用戶交互,另外幾個執行緒每個執行緒負責和一個網路主機通信。
以前我們講過,main函數和信號處理函數是同一個行程(Process)位址空間中的多個控制流程,多執行緒也是如此,但是比信號處理函數更加靈活,信號處理函數的控制流程只是在信號遞達時產生,在處理完信號之後就結束,而多執行緒的控制流程可以長期並存,作業系統會在各執行緒之間調度和切換,就像在多個行程(Process)之間調度和切換一樣。由於同一行程(Process)的多個執行緒共用同一位址空間,因此Text Segment、Data Segment都是共用的,如果定義一個函數,在各執行緒中都可以調用,如果定義一個全域變數,在各執行緒中都可以訪問到,除此之外,各執行緒還共用以下行程(Process)資源和環境:
l 檔描述符表
l 每種信號的處理方式(SIG_IGN、SIG_DFL或者自訂的信號處理函數)
l 當前工作目錄
l 用戶id和組id
但有些資源是每個執行緒各有一份的:
l 執行緒id
l 上下文,包括各種暫存器的值、程式計數器和堆疊/棧(stack)指標
l 堆疊/棧(stack)空間
l errno變數
l 信號遮罩字
l 調度優先順序
我們將要學習的執行緒庫函數是由POSIX標準定義的,稱為POSIX thread或者pthread。
在Linux上執行緒函數位於libpthread共用庫中,因此在編譯時要加上 -lpthread選項。
2.2 2. 執行緒控制
2.2.1 2.1. 創建執行緒
#include <pthread.h>
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void*),
void *restrict arg);
ex: pthread_create(&ntid, NULL, thr_fn, "new thread: ");
返回值:成功返回0,失敗返回錯誤號。
以前學過的系統函數都是成功返回0,失敗返回-1,而錯誤號保存在全域變數errno中,而pthread庫的函數都是通過返回值返回錯誤號,雖然每個執行緒也都有一個errno,但這是為了相容其它函數介面而提供的,pthread庫本身並不使用它,通過返回值返回錯誤碼更加清晰。
在一個執行緒中調用pthread_create()創建新的執行緒後,當前執行緒從pthread_create()返回繼續往下執行,而新的執行緒所執行的代碼由我們傳給pthread_create的函數指標start_routine決定。
start_routine函數接收一個參數,是通過pthread_create的arg參數傳遞給它的,該參數的類型為void *,這個指標按什麼類型解釋由調用者自己定義。start_routine的返回數值型別也是void *,這個指標的含義同樣由調用者自己定義。start_routine返回時,這個執行緒就退出了,其它執行緒可以調用pthread_join得到start_routine的返回值,類似于父進程調用wait(2)得到子進程的退出狀態,稍後詳細介紹pthread_join。
pthread_create成功返回後,新創建的執行緒的id被填寫到thread參數所指向的記憶體單元。我們知道行程(Process) id的類型是pid_t,每個行程(Process)的id在整個系統中是唯一的,調用getpid(2)可以獲得當前進程的id,是一個正整數值。
執行緒id的類型是thread_t,它只在當前行程(Process)中保證是唯一的,在不同的系統中thread_t這個類型有不同的實現,它可能是一個整數值,也可能是一個結構體,也可能是一個位址,所以不能簡單地當成整數用printf列印,調用pthread_self(3)可以獲得當前執行緒的id。
attr參數表示執行緒屬性,本章不深入討論執行緒屬性,所有代碼例子都傳NULL給attr參數,表示執行緒屬性取缺省值,感興趣的讀者可以參考[APUE2e]。首先看一個簡單的例子:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
pthread_t ntid;
void printids(const char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid,
(unsigned int)tid, (unsigned int)tid);
}
void *thr_fn(void *arg)
{
printids(arg);
return NULL;
}
int main(void)
{
int err;
err = pthread_create(&ntid, NULL, thr_fn, "new thread: ");
if (err != 0) {
fprintf(stderr, "can't create thread: %s\n", strerror(err));
exit(1);
}
printids("main thread:");
sleep(1);
return 0;
}
編譯運行結果如下:
$ gcc main.c -lpthread
$ ./a.out
main thread: pid 7398 tid 3084450496 (0xb7d8fac0)
new thread: pid 7398 tid 3084446608 (0xb7d8eb90)
可知在Linux上,thread_t類型是一個位址值,屬於同一行程(Process)的多個執行緒調用getpid(2)可以得到相同的行程號,而調用pthread_self(3)得到的執行緒號各不相同。
由於pthread_create的錯誤碼不保存在errno中,因此不能直接用perror(3)列印錯誤資訊,可以先用strerror(3)把錯誤碼轉換成錯誤資訊再列印。
如果任意一個執行緒調用了exit或_exit,則整個行程的所有執行緒都終止,由於從main函數return也相當於調用exit,為了防止新創建的執行緒還沒有得到執行就終止,我們在main函數return之前延時1秒,這只是一種權宜之計,即使主執行緒等待1秒,內核也不一定會調度新創建的執行緒執行,下一節我們會看到更好的辦法。
???思考題:主執行緒在一個全域變數ntid中保存了新創建的執行緒的id,如果新創建的執行緒不調用pthread_self而是直接列印這個ntid,能不能達到同樣的效果?
2.2.2 2.2. 終止執行緒
如果需要只終止某個執行緒而不終止整個行程,可以有三種方法:
· 從執行緒函數return。這種方法對主執行緒不適用,從main函數return相當於調用exit。
· 一個執行緒可以調用pthread_cancel終止同一行程中的另一個執行緒。
· 執行緒可以調用pthread_exit終止自己。
用pthread_cancel終止一個執行緒分同步和非同步兩種情況,比較複雜,本章不打算詳細介紹,讀者可以參考[APUE2e]。
下麵介紹pthread_exit的和pthread_join的用法。
#include <pthread.h>
void pthread_exit(void *value_ptr);
value_ptr是void *類型,和執行緒函數返回值的用法一樣,其它執行緒可以調用pthread_join獲得這個指標。
需要注意,pthread_exit或者return返回的指標所指向的記憶體單元必須是全域的或者是用malloc分配的,不能在執行緒函數的堆疊/棧(stack)上分配,因為當其它執行緒得到這個返回指標時執行緒函數已經退出了。
#include <pthread.h>
int pthread_join(pthread_t thread, void **value_ptr);
功能:等待指定的執行緒結束。
返回值:成功返回0,失敗返回錯誤號
調用該函數的執行緒將掛起等待,直到id為thread的執行緒終止。
thread執行緒以不同的方法終止,通過pthread_join得到的終止狀態是不同的,總結如下:
· 如果thread執行緒通過return返回,value_ptr所指向的單元裡存放的是thread執行緒函數的返回值。
· 如果thread執行緒被別的執行緒調用pthread_cancel異常終止掉,value_ptr所指向的單元裡存放的是常數PTHREAD_CANCELED。
· 如果thread執行緒是自己調用pthread_exit終止的,value_ptr所指向的單元存放的是傳給pthread_exit的參數。
如果對thread執行緒的終止狀態不感興趣,可以傳NULL給value_ptr參數。
看下面的例子(省略了出錯處理):
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *thr_fn1(void *arg)
{
printf("thread 1 returning\n");
return (void *)1;
}
void *thr_fn2(void *arg)
{
printf("thread 2 exiting\n");
pthread_exit((void *)2);
}
void *thr_fn3(void *arg)
{
while(1) {
printf("thread 3 writing\n");
sleep(1);
}
}
int main(void)
{
pthread_t tid;
void *tret;
pthread_create(&tid, NULL, thr_fn1, NULL);//for return
pthread_join(tid, &tret);
printf("thread 1 exit code %d\n", (int)tret);
pthread_create(&tid, NULL, thr_fn2, NULL);// pthread_exit
pthread_join(tid, &tret);
printf("thread 2 exit code %d\n", (int)tret);
pthread_create(&tid, NULL, thr_fn3, NULL);//for while(1)
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &tret);
printf("thread 3 exit code %d\n", (int)tret);
return 0;
}
運行結果是:
$ ./a.out
thread 1 returning
thread 1 exit code 1
thread 2 exiting
thread 2 exit code 2
thread 3 writing
thread 3 writing
thread 3 writing
thread 3 exit code -1
$ ./a.out (無pthread_join時)
thread 1 exit code -1077688820
thread 2 exit code -1077688820
pid 3086134160 tid 3086134160 (0x3d0f00)
thread 1 returning
pid 3075644304 tid 3075644304 (0x0)
thread 2 exiting
pid 3065154448 tid 3065154448 (0x0)
thread 3 writing
thread 3 writing
thread 3 writing
thread 3 exit code -1077688820
可見在Linux的pthread庫中常數PTHREAD_CANCELED的值是-1。可以在標頭檔pthread.h中找到它的定義:
#define PTHREAD_CANCELED ((void *) -1)
一般情況下,執行緒終止後,其終止狀態一直保留到其它執行緒調用pthread_join獲取它的狀態為止。
但是執行緒也可以被置為detach狀態,這樣的執行緒一旦終止就立刻回收它佔用的所有資源,而不保留終止狀態。不能對一個已經處於detach狀態的執行緒調用pthread_join,這樣的調用將返回EINVAL。對一個尚未detach的執行緒調用pthread_join或pthread_detach都可以把該執行緒置為detach狀態,也就是說,不能對同一執行緒調用兩次pthread_join,或者如果已經對一個執行緒調用了pthread_detach就不能再調用pthread_join了。
#include <pthread.h>
int pthread_detach(pthread_t tid);
返回值:成功返回0,失敗返回錯誤號。
2.3 3. 執行緒間同步
同步執行緒的方式: 封鎖呼叫(join)互斥鎖(mutex)條件變數(condition variable)。
Join語意是加入,但跟官網的解釋不太一樣,Thread.Join 方法在官網的解釋是:封鎖呼叫執行緒,直到執行緒結束為止。
2.3.1 3.1. mutex(互斥鎖)
多個執行緒同時訪問共用資料時可能會訪問衝突(Access Conflict),這跟前面講信號時所說的可重入性是同樣的問題。比如兩個執行緒都要把某個全域變數增加1,這個操作在某平臺需要三條指令完成:
1. 從記憶體讀變數值到暫存器
2. 暫存器的值加1
3. 將暫存器的值寫回記憶體
假設兩個執行緒在多處理器平臺上同時執行這三條指令,則可能導致下圖所示的結果,最後變數只加了一次而非兩次。
圖 35.1. 並行訪問衝突(Access Conflict)
思考一下,如果這兩個執行緒在單一處理器平臺上執行,能夠避免這樣的問題嗎?
我們通過一個簡單的程式觀察這一現象。上圖所描述的現象從理論上是存在這種可能的,但實際運行程式時很難觀察到,為了使現象更容易觀察到,我們把上述三條指令做的事情用更多條指令來做:
val = counter;
printf("%x: %d\n", (unsigned int)pthread_self(), val + 1);
counter = val + 1;
我們在“讀取變數的值”和“把變數的新值保存回去”這兩步操作之間插入一個printf調用,它會執行write系統調用進內核,為內核調度別的執行緒執行提供了一個很好的時機。我們在一個迴圈中重複上述操作幾千次,就會觀察到訪問衝突的現象。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NLOOP 5000
int counter; /* incremented by threads */
void *doit(void *);
int main(int argc, char **argv)
{
pthread_t tidA, tidB;
pthread_create(&tidA, NULL, &doit, NULL);
pthread_create(&tidB, NULL, &doit, NULL);
/* wait for both threads to terminate */
pthread_join(tidA, NULL);
pthread_join(tidB, NULL);
return 0;
}
void *doit(void *vptr)
{
int i, val;
/*
* Each thread fetches, prints, and increments the counter NLOOP times.
* The value of the counter should increase monotonically.
*/
for (i = 0; i < NLOOP; i++) {
val = counter;
printf("%x: %d\n", (unsigned int)pthread_self(), val + 1);
counter = val + 1;
}
return NULL;
}
我們創建兩個執行緒,各自把counter增加5000次,正常情況下最後counter應該等於10000,但事實上每次運行該程式的結果都不一樣,有時候數到5000多,有時候數到6000多。
$ ./a.out
b76acb90: 1
b76acb90: 2
b76acb90: 3
b76acb90: 4
b76acb90: 5
b7eadb90: 1
b7eadb90: 2
b7eadb90: 3
b7eadb90: 4
b7eadb90: 5
b76acb90: 6
b76acb90: 7
b7eadb90: 6
b76acb90: 8
...
對於多執行緒的程式,訪問衝突(Access Conflict)的問題是很普遍的,解決的辦法是引入互斥鎖(Mutex,Mutual Exclusive Lock),獲得鎖(Mutex)的執行緒可以完成“讀-修改-寫”的操作,然後釋放鎖給其它執行緒,沒有獲得鎖的執行緒只能等待而不能訪問共用資料,這樣“讀-修改-寫”三步操作組成一個原子(atom)操作,要麼都執行,要麼都不執行,不會執行到中間被打斷,也不會在其它處理器上並行做這個操作。
Mutex用pthread_mutex_t類型的變數表示,可以這樣初始化(init)和銷毀(destroy):
#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;
返回值:成功返回0,失敗返回錯誤號。
pthread_mutex_init函數對Mutex做初始化,參數attr設定Mutex的屬性,如果attr為NULL則表示缺省/預設(default)屬性,本章不詳細介紹Mutex屬性,感興趣的讀者可以參考[APUE2e]。
用pthread_mutex_init函數初始化的Mutex可以用pthread_mutex_destroy銷毀。
如果Mutex變數是靜態配置的(全域變數或static變數),也可以用巨集定義PTHREAD_MUTEX_INITIALIZER來初始化,相當於用pthread_mutex_init初始化並且attr參數為NULL。Mutex的加鎖和解鎖操作可以用下列函數:
#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);
返回值:成功返回0,失敗返回錯誤號。
一個執行緒可以調用pthread_mutex_lock獲得Mutex,如果這時另一個執行緒已經調用pthread_mutex_lock獲得了該Mutex,則當前執行緒需要掛起等待,直到另一個執行緒調用pthread_mutex_unlock釋放Mutex,當前執行緒被喚醒,才能獲得該Mutex並繼續執行。
如果一個執行緒既想獲得鎖,又不想掛起等待,可以調用pthread_mutex_trylock,如果Mutex已經被另一個執行緒獲得,這個函數會失敗返回EBUSY,而不會使執行緒掛起等待。
現在我們用Mutex解決先前的問題:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NLOOP 5000
int counter; /* incremented by threads */
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
void *doit(void *);
int main(int argc, char **argv)
{
pthread_t tidA, tidB;
pthread_create(&tidA, NULL, doit, NULL);
pthread_create(&tidB, NULL, doit, NULL);
/* wait for both threads to terminate */
pthread_join(tidA, NULL);
pthread_join(tidB, NULL);
return 0;
}
void *doit(void *vptr)
{
int i, val;
/*
* Each thread fetches, prints, and increments the counter NLOOP times.
* The value of the counter should increase monotonically.
*/
for (i = 0; i < NLOOP; i++) {
pthread_mutex_lock(&counter_mutex);
val = counter;
printf("%x: %d\n", (unsigned int)pthread_self(), val + 1);
counter = val + 1;
pthread_mutex_unlock(&counter_mutex);
}
return NULL;
}
這樣運行結果就正常了,每次運行都能數到10000。
看到這裡,讀者一定會好奇:
Mutex的兩個基本操作lock和unlock是如何實現的呢?
假設Mutex變數的值為1表示互斥鎖空閒,這時某個進程調用lock可以獲得鎖,而Mutex的值為0表示互斥鎖已經被某個執行緒獲得,其它執行緒再調用lock只能掛起等待。那麼lock和unlock的偽代碼如下:
lock:
if(mutex > 0){
mutex = 0;
return 0;
} else
掛起等待;
goto lock;
unlock:
mutex = 1;
喚醒等待Mutex的執行緒;
return 0;
unlock操作中喚醒等待中的執行緒的步驟可以有不同的實現,可以只喚醒一個等待中的執行緒,也可以喚醒所有等待該Mutex的執行緒,然後讓被喚醒的這些執行緒去競爭獲得這個Mutex,競爭失敗的執行緒繼續掛起等待。
細心的讀者應該已經看出問題了:
對Mutex變數的讀取、判斷和修改不是原子操作。
如果兩個執行緒同時調用lock,這時Mutex是1,兩個執行緒都判斷mutex>0成立,然後其中一個執行緒置mutex=0,而另一個執行緒並不知道這一情況,也置mutex=0,於是兩個執行緒都以為自己獲得了鎖。
為了實現互斥鎖操作,大多數體系結構都提供了swap或exchange指令,該指令的作用是把暫存器和記憶體單元的資料相交換,由於只有一條指令,保證了原子性,即使是多處理器平臺,訪問記憶體的匯流排週期也有先後,一個處理器上的交換指令執行時另一個處理器的交換指令只能等待匯流排週期。現在我們把lock和unlock的偽代碼改一下(以x86的xchg指令為例):
lock:
movb $0, %al
xchgb %al, mutex
if(al寄存器的內容 > 0){
return 0;
} else
掛起等待;
goto lock;
unlock:
movb $1, mutex
喚醒等待Mutex的執行緒;
return 0;
unlock中的釋放鎖操作同樣只用一條指令實現,以保證它的原子性。
也許還有讀者好奇,“掛起等待”和“喚醒等待中的執行緒”的操作如何實現?每個Mutex有一個等待佇列,一個執行緒要在Mutex上掛起等待,首先在把自己加入等待佇列中,然後置執行緒狀態為睡眠,然後調用調度器函數切換到別的執行緒。一個執行緒要喚醒等待佇列中的其它執行緒,只需從等待佇列中取出一項,把它的狀態從睡眠改為就緒,加入就緒佇列,那麼下次調度器函數執行時就有可能切換到被喚醒的執行緒。
一般情況下,如果同一個執行緒先後兩次調用lock,在第二次調用時,由於鎖已經被佔用,該執行緒會掛起等待別的執行緒釋放鎖,然而鎖正是被自己佔用著的,該執行緒又被掛起而沒有機會釋放鎖,因此就永遠處於掛起等候狀態了,這叫做鎖死(Deadlock)。另一種典型的鎖死情形是這樣:執行緒A獲得了鎖1,執行緒B獲得了鎖2,這時執行緒A調用lock試圖獲得鎖2,結果是需要掛起等待中的執行緒B釋放鎖2,而這時執行緒B也調用lock試圖獲得鎖1,結果是需要掛起等待中的執行緒A釋放鎖1,於是執行緒A和B都永遠處於掛起狀態了。不難想像,如果涉及到更多的執行緒和更多的鎖,有沒有可能鎖死的問題將會變得複雜和難以判斷。
寫程式時應該儘量避免同時獲得多個鎖,如果一定有必要這麼做,則有一個原則:如果所有執行緒在需要多個鎖時都按相同的先後順序(常見的是按Mutex變數的位址順序)獲得鎖,則不會出現鎖死。
比如一個程式中用到鎖1、鎖2、鎖3,它們所對應的Mutex變數的位址是鎖1<鎖2<鎖3,那麼所有執行緒在需要同時獲得2個或3個鎖時都應該按鎖1、鎖2、鎖3的順序獲得。
如果要為所有的鎖確定一個先後順序比較困難,則應該儘量使用pthread_mutex_trylock調用代替pthread_mutex_lock調用,以免鎖死。
1.1.1 3.2. Condition Variable(條件變數)
執行緒間的同步還有這樣一種情況:
執行緒A需要等某個條件成立才能繼續往下執行,現在這個條件不成立,執行緒A就阻塞等待,
而執行緒B在執行過程中使這個條件成立了,就喚醒執行緒A繼續執行。在pthread庫中通過條件變數(Condition Variable)來阻塞等待一個條件,或者喚醒等待這個條件的執行緒。Condition Variable用pthread_cond_t類型的變數表示,可以這樣初始化和銷毀:
#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;
返回值:成功返回0,失敗返回錯誤號。
和Mutex的初始化和銷毀類似,pthread_cond_init函數初始化一個Condition Variable,attr參數為NULL則表示缺省屬性,
pthread_cond_destroy函數銷毀一個Condition Variable。如果Condition Variable是靜態配置的,也可以用巨集定義PTHEAD_COND_INITIALIZER初始化,相當於用pthread_cond_init函數初始化並且attr參數為NULL。
Condition Variable的操作可以用下列函數:
#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
返回值:成功返回0,失敗返回錯誤號。
可見,一個Condition Variable總是和一個Mutex搭配使用的。
一個執行緒可以調用pthread_cond_wait在一個Condition Variable上阻塞等待,這個函數做以下三步操作:
1. 釋放Mutex
2. 阻塞等待
3. 當被喚醒時,重新獲得Mutex並返回
pthread_cond_timedwait函數還有一個額外的參數可以設定等待超時(Timeout),如果到達了abstime所指定的時刻仍然沒有別的執行緒來喚醒當前執行緒,就返回ETIMEDOUT。
一個執行緒可以調用pthread_cond_signal喚醒在某個Condition Variable上等待的另一個執行緒,
也可以調用pthread_cond_broadcast喚醒在這個Condition Variable上等待的所有執行緒。
下面的程式演示了一個生產者-消費者的例子,生產者生產一個結構體串在鏈表的表頭上,消費者從表頭取走結構體。
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
struct msg {
struct msg *next;
int num;
};
struct msg *head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *consumer(void *p)
{
struct msg *mp;
for (;;) {
pthread_mutex_lock(&lock);
while (head == NULL)
pthread_cond_wait(&has_product, &lock);
mp = head;
head = mp->next;
pthread_mutex_unlock(&lock);
printf("Consume %d\n", mp->num);
free(mp);
sleep(rand() % 5);
}
}
void *producer(void *p)
{
struct msg *mp;
for (;;) {
mp = malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1;
printf("Produce %d\n", mp->num);
pthread_mutex_lock(&lock);
mp->next = head;
head = mp;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_product);
sleep(rand() % 5);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid;
srand(time(NULL));
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
執行結果如下:
$ ./a.out
Produce 744
Consume 744
Produce 567
Produce 881
Consume 881
Produce 911
Consume 911
Consume 567
Produce 698
Consume 698
1.1.1.1 習題
1、 在本節的例子中,生產者和消費者訪問鏈表的順序是LIFO的,請修改程式,把訪問順序改成FIFO。
《linux c 編程一站式學習》課後部分習題解答
http://rritw.com/a/bianchengyuyan/C__/20130416/340992.html
C++ Code
/*************************************************************************
> File Name: consumer.c
> Author: Simba
> Mail: [email protected]
> Created Time: 2012年12月19日 星期三 00時15分47秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
/* 程式演示了一個生產者-消費者的例子,生產者生產一個結構體串在鏈表的表頭上,消費者
從表尾取走結構體。注意: 不一定產生一次就取走一次,雖然產生一次就喚醒一次消費者
,但有可能此時並未調度消費者線程運行,但取走的一定是表尾的結構體,即最快生產剩下未被取走的即FIFO */
struct msg
{
struct msg *next;
int num;
};
struct msg *head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *consumer(void *p)
{
struct msg *mp;
for (;;)
{
pthread_mutex_lock(&lock);
while (head == NULL)
pthread_cond_wait(&has_product, &lock);
if (head->next != NULL)
{
mp = head->next;
head->next = mp->next;
}
else
{
mp = head;
head = mp->next;
}
pthread_mutex_unlock(&lock);
printf("Consume %d\n", mp->num);
free(mp);
sleep(rand() % 5);
}
}
void *producer(void *p)
{
struct msg *mp;
for (;;)
{
mp = malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1;
printf("Produce %d \n", mp->num);
pthread_mutex_lock(&lock);
mp->next = head;
head = mp;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_product);
sleep(rand() % 5);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid;
srand(time(NULL));
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
1.1.1 3.3. Semaphore
Mutex變數是非0即1的,可看作一種資源的可用數量,初始化時Mutex是1,表示有一個可用資源,加鎖時獲得該資源,將Mutex減到0,表示不再有可用資源,解鎖時釋放該資源,將Mutex重新加到1,表示又有了一個可用資源。
信號量(Semaphore)和Mutex類似,表示可用資源的數量,和Mutex不同的是這個數量可以大於1。
本節介紹的是POSIX semaphore庫函數,詳見sem_overview(7),這種信號量不僅可用於同一進程的執行緒間同步,也可用於不同進程間的同步。
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t * sem);
int sem_destroy(sem_t * sem);
semaphore變數的類型為sem_t,
sem_init()初始化一個semaphore變數,value參數表示可用資源的數量,pshared參數為0表示信號量用於同一進程的執行緒間同步,本節只介紹這種情況。
在用完semaphore變數之後應該調用sem_destroy()釋放與semaphore相關的資源。
調用sem_wait()可以獲得資源,使semaphore的值減1,
如果調用sem_wait()時semaphore的值已經是0,則掛起等待。
如果不希望掛起等待,可以調用sem_trywait()。
調用sem_post()可以釋放資源,使semaphore的值加1,同時喚醒掛起等待的執行緒。
上一節生產者-消費者的例子是基於鏈表的,其空間可以動態分配,現在基於固定大小的環形佇列重寫這個程式:
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>
#define NUM 5
int queue[NUM];
sem_t blank_number, product_number;
void *producer(void *arg)
{
int p = 0;
while (1) {
sem_wait(&blank_number);
queue[p] = rand() % 1000 + 1;
printf("Produce %d\n", queue[p]);
sem_post(&product_number);
p = (p+1)%NUM;
sleep(rand()%5);
}
}
void *consumer(void *arg)
{
int c = 0;
while (1) {
sem_wait(&product_number);
printf("Consume %d\n", queue[c]);
queue[c] = 0;
sem_post(&blank_number);
c = (c+1)%NUM;
sleep(rand()%5);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid;
sem_init(&blank_number, 0, NUM);
sem_init(&product_number, 0, 0);
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
sem_destroy(&blank_number);
sem_destroy(&product_number);
return 0;
}
1.1.1.1 習題
1、本節和上一節的例子給出一個重要的提示:
用Condition Variable可以實現Semaphore。
請用Condition Variable實現Semaphore,然後用自己實現的Semaphore重寫本節的程式。
1.1.2 3.4. 其它執行緒間同步機制
如果共用資料是唯讀的,那麼各執行緒讀到的資料應該總是一致的,不會出現訪問衝突。只要有一個執行緒可以改寫資料,就必須考慮執行緒間同步的問題。由此引出了讀者寫者鎖(Reader-Writer Lock)的概念,Reader之間並不互斥,可以同時讀共用資料,而Writer是獨佔的(exclusive),在Writer修改資料時其它Reader或Writer不能訪問資料,可見Reader-Writer Lock比Mutex具有更好的併發性。
用掛起等待的方式解決訪問衝突不見得是最好的辦法,因為這樣畢竟會影響系統的併發性,在某些情況下解決訪問衝突的問題可以儘量避免掛起某個執行緒,例如Linux內核的Seqlock、RCU(read-copy-update)等機制。
關於這些同步機制的細節,有興趣的讀者可以參考[APUE2e]和[ULK]。
我們通過一個簡單的程式觀察這一現象。上圖所描述的現象從理論上是存在這種可能的,但實際運行程式時很難觀察到,為了使現象更容易觀察到,我們把上述三條指令做的事情用更多條指令來做:
val = counter;
printf("%x: %d\n", (unsigned int)pthread_self(), val + 1);
counter = val + 1;
我們在“讀取變數的值”和“把變數的新值保存回去”這兩步操作之間插入一個printf調用,它會執行write系統調用進內核,為內核調度別的執行緒執行提供了一個很好的時機。我們在一個迴圈中重複上述操作幾千次,就會觀察到訪問衝突的現象。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NLOOP 5000
int counter; /* incremented by threads */
void *doit(void *);
int main(int argc, char **argv)
{
pthread_t tidA, tidB;
pthread_create(&tidA, NULL, &doit, NULL);
pthread_create(&tidB, NULL, &doit, NULL);
/* wait for both threads to terminate */
pthread_join(tidA, NULL);
pthread_join(tidB, NULL);
return 0;
}
void *doit(void *vptr)
{
int i, val;
/*
* Each thread fetches, prints, and increments the counter NLOOP times.
* The value of the counter should increase monotonically.
*/
for (i = 0; i < NLOOP; i++) {
val = counter;
printf("%x: %d\n", (unsigned int)pthread_self(), val + 1);
counter = val + 1;
}
return NULL;
}
我們創建兩個執行緒,各自把counter增加5000次,正常情況下最後counter應該等於10000,但事實上每次運行該程式的結果都不一樣,有時候數到5000多,有時候數到6000多。
$ ./a.out
b76acb90: 1
b76acb90: 2
b76acb90: 3
b76acb90: 4
b76acb90: 5
b7eadb90: 1
b7eadb90: 2
b7eadb90: 3
b7eadb90: 4
b7eadb90: 5
b76acb90: 6
b76acb90: 7
b7eadb90: 6
b76acb90: 8
...
對於多執行緒的程式,訪問衝突(Access Conflict)的問題是很普遍的,解決的辦法是引入互斥鎖(Mutex,Mutual Exclusive Lock),獲得鎖(Mutex)的執行緒可以完成“讀-修改-寫”的操作,然後釋放鎖給其它執行緒,沒有獲得鎖的執行緒只能等待而不能訪問共用資料,這樣“讀-修改-寫”三步操作組成一個原子(atom)操作,要麼都執行,要麼都不執行,不會執行到中間被打斷,也不會在其它處理器上並行做這個操作。
Mutex用pthread_mutex_t類型的變數表示,可以這樣初始化(init)和銷毀(destroy):
#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;
返回值:成功返回0,失敗返回錯誤號。
pthread_mutex_init函數對Mutex做初始化,參數attr設定Mutex的屬性,如果attr為NULL則表示缺省/預設(default)屬性,本章不詳細介紹Mutex屬性,感興趣的讀者可以參考[APUE2e]。
用pthread_mutex_init函數初始化的Mutex可以用pthread_mutex_destroy銷毀。
如果Mutex變數是靜態配置的(全域變數或static變數),也可以用巨集定義PTHREAD_MUTEX_INITIALIZER來初始化,相當於用pthread_mutex_init初始化並且attr參數為NULL。Mutex的加鎖和解鎖操作可以用下列函數:
#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);
返回值:成功返回0,失敗返回錯誤號。
一個執行緒可以調用pthread_mutex_lock獲得Mutex,如果這時另一個執行緒已經調用pthread_mutex_lock獲得了該Mutex,則當前執行緒需要掛起等待,直到另一個執行緒調用pthread_mutex_unlock釋放Mutex,當前執行緒被喚醒,才能獲得該Mutex並繼續執行。
如果一個執行緒既想獲得鎖,又不想掛起等待,可以調用pthread_mutex_trylock,如果Mutex已經被另一個執行緒獲得,這個函數會失敗返回EBUSY,而不會使執行緒掛起等待。
現在我們用Mutex解決先前的問題:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NLOOP 5000
int counter; /* incremented by threads */
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
void *doit(void *);
int main(int argc, char **argv)
{
pthread_t tidA, tidB;
pthread_create(&tidA, NULL, doit, NULL);
pthread_create(&tidB, NULL, doit, NULL);
/* wait for both threads to terminate */
pthread_join(tidA, NULL);
pthread_join(tidB, NULL);
return 0;
}
void *doit(void *vptr)
{
int i, val;
/*
* Each thread fetches, prints, and increments the counter NLOOP times.
* The value of the counter should increase monotonically.
*/
for (i = 0; i < NLOOP; i++) {
pthread_mutex_lock(&counter_mutex);
val = counter;
printf("%x: %d\n", (unsigned int)pthread_self(), val + 1);
counter = val + 1;
pthread_mutex_unlock(&counter_mutex);
}
return NULL;
}
這樣運行結果就正常了,每次運行都能數到10000。
看到這裡,讀者一定會好奇:
Mutex的兩個基本操作lock和unlock是如何實現的呢?
假設Mutex變數的值為1表示互斥鎖空閒,這時某個進程調用lock可以獲得鎖,而Mutex的值為0表示互斥鎖已經被某個執行緒獲得,其它執行緒再調用lock只能掛起等待。那麼lock和unlock的偽代碼如下:
lock:
if(mutex > 0){
mutex = 0;
return 0;
} else
掛起等待;
goto lock;
unlock:
mutex = 1;
喚醒等待Mutex的執行緒;
return 0;
unlock操作中喚醒等待中的執行緒的步驟可以有不同的實現,可以只喚醒一個等待中的執行緒,也可以喚醒所有等待該Mutex的執行緒,然後讓被喚醒的這些執行緒去競爭獲得這個Mutex,競爭失敗的執行緒繼續掛起等待。
細心的讀者應該已經看出問題了:
對Mutex變數的讀取、判斷和修改不是原子操作。
如果兩個執行緒同時調用lock,這時Mutex是1,兩個執行緒都判斷mutex>0成立,然後其中一個執行緒置mutex=0,而另一個執行緒並不知道這一情況,也置mutex=0,於是兩個執行緒都以為自己獲得了鎖。
為了實現互斥鎖操作,大多數體系結構都提供了swap或exchange指令,該指令的作用是把暫存器和記憶體單元的資料相交換,由於只有一條指令,保證了原子性,即使是多處理器平臺,訪問記憶體的匯流排週期也有先後,一個處理器上的交換指令執行時另一個處理器的交換指令只能等待匯流排週期。現在我們把lock和unlock的偽代碼改一下(以x86的xchg指令為例):
lock:
movb $0, %al
xchgb %al, mutex
if(al寄存器的內容 > 0){
return 0;
} else
掛起等待;
goto lock;
unlock:
movb $1, mutex
喚醒等待Mutex的執行緒;
return 0;
unlock中的釋放鎖操作同樣只用一條指令實現,以保證它的原子性。
也許還有讀者好奇,“掛起等待”和“喚醒等待中的執行緒”的操作如何實現?每個Mutex有一個等待佇列,一個執行緒要在Mutex上掛起等待,首先在把自己加入等待佇列中,然後置執行緒狀態為睡眠,然後調用調度器函數切換到別的執行緒。一個執行緒要喚醒等待佇列中的其它執行緒,只需從等待佇列中取出一項,把它的狀態從睡眠改為就緒,加入就緒佇列,那麼下次調度器函數執行時就有可能切換到被喚醒的執行緒。
一般情況下,如果同一個執行緒先後兩次調用lock,在第二次調用時,由於鎖已經被佔用,該執行緒會掛起等待別的執行緒釋放鎖,然而鎖正是被自己佔用著的,該執行緒又被掛起而沒有機會釋放鎖,因此就永遠處於掛起等候狀態了,這叫做鎖死(Deadlock)。另一種典型的鎖死情形是這樣:執行緒A獲得了鎖1,執行緒B獲得了鎖2,這時執行緒A調用lock試圖獲得鎖2,結果是需要掛起等待中的執行緒B釋放鎖2,而這時執行緒B也調用lock試圖獲得鎖1,結果是需要掛起等待中的執行緒A釋放鎖1,於是執行緒A和B都永遠處於掛起狀態了。不難想像,如果涉及到更多的執行緒和更多的鎖,有沒有可能鎖死的問題將會變得複雜和難以判斷。
寫程式時應該儘量避免同時獲得多個鎖,如果一定有必要這麼做,則有一個原則:如果所有執行緒在需要多個鎖時都按相同的先後順序(常見的是按Mutex變數的位址順序)獲得鎖,則不會出現鎖死。
比如一個程式中用到鎖1、鎖2、鎖3,它們所對應的Mutex變數的位址是鎖1<鎖2<鎖3,那麼所有執行緒在需要同時獲得2個或3個鎖時都應該按鎖1、鎖2、鎖3的順序獲得。
如果要為所有的鎖確定一個先後順序比較困難,則應該儘量使用pthread_mutex_trylock調用代替pthread_mutex_lock調用,以免鎖死。
1.1.1 3.2. Condition Variable(條件變數)
執行緒間的同步還有這樣一種情況:
執行緒A需要等某個條件成立才能繼續往下執行,現在這個條件不成立,執行緒A就阻塞等待,
而執行緒B在執行過程中使這個條件成立了,就喚醒執行緒A繼續執行。在pthread庫中通過條件變數(Condition Variable)來阻塞等待一個條件,或者喚醒等待這個條件的執行緒。Condition Variable用pthread_cond_t類型的變數表示,可以這樣初始化和銷毀:
#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;
返回值:成功返回0,失敗返回錯誤號。
和Mutex的初始化和銷毀類似,pthread_cond_init函數初始化一個Condition Variable,attr參數為NULL則表示缺省屬性,
pthread_cond_destroy函數銷毀一個Condition Variable。如果Condition Variable是靜態配置的,也可以用巨集定義PTHEAD_COND_INITIALIZER初始化,相當於用pthread_cond_init函數初始化並且attr參數為NULL。
Condition Variable的操作可以用下列函數:
#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
返回值:成功返回0,失敗返回錯誤號。
可見,一個Condition Variable總是和一個Mutex搭配使用的。
一個執行緒可以調用pthread_cond_wait在一個Condition Variable上阻塞等待,這個函數做以下三步操作:
1. 釋放Mutex
2. 阻塞等待
3. 當被喚醒時,重新獲得Mutex並返回
pthread_cond_timedwait函數還有一個額外的參數可以設定等待超時(Timeout),如果到達了abstime所指定的時刻仍然沒有別的執行緒來喚醒當前執行緒,就返回ETIMEDOUT。
一個執行緒可以調用pthread_cond_signal喚醒在某個Condition Variable上等待的另一個執行緒,
也可以調用pthread_cond_broadcast喚醒在這個Condition Variable上等待的所有執行緒。
下面的程式演示了一個生產者-消費者的例子,生產者生產一個結構體串在鏈表的表頭上,消費者從表頭取走結構體。
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
struct msg {
struct msg *next;
int num;
};
struct msg *head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *consumer(void *p)
{
struct msg *mp;
for (;;) {
pthread_mutex_lock(&lock);
while (head == NULL)
pthread_cond_wait(&has_product, &lock);
mp = head;
head = mp->next;
pthread_mutex_unlock(&lock);
printf("Consume %d\n", mp->num);
free(mp);
sleep(rand() % 5);
}
}
void *producer(void *p)
{
struct msg *mp;
for (;;) {
mp = malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1;
printf("Produce %d\n", mp->num);
pthread_mutex_lock(&lock);
mp->next = head;
head = mp;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_product);
sleep(rand() % 5);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid;
srand(time(NULL));
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
執行結果如下:
$ ./a.out
Produce 744
Consume 744
Produce 567
Produce 881
Consume 881
Produce 911
Consume 911
Consume 567
Produce 698
Consume 698
1.1.1.1 習題
1、 在本節的例子中,生產者和消費者訪問鏈表的順序是LIFO的,請修改程式,把訪問順序改成FIFO。
《linux c 編程一站式學習》課後部分習題解答
http://rritw.com/a/bianchengyuyan/C__/20130416/340992.html
C++ Code
/*************************************************************************
> File Name: consumer.c
> Author: Simba
> Mail: [email protected]
> Created Time: 2012年12月19日 星期三 00時15分47秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
/* 程式演示了一個生產者-消費者的例子,生產者生產一個結構體串在鏈表的表頭上,消費者
從表尾取走結構體。注意: 不一定產生一次就取走一次,雖然產生一次就喚醒一次消費者
,但有可能此時並未調度消費者線程運行,但取走的一定是表尾的結構體,即最快生產剩下未被取走的即FIFO */
struct msg
{
struct msg *next;
int num;
};
struct msg *head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *consumer(void *p)
{
struct msg *mp;
for (;;)
{
pthread_mutex_lock(&lock);
while (head == NULL)
pthread_cond_wait(&has_product, &lock);
if (head->next != NULL)
{
mp = head->next;
head->next = mp->next;
}
else
{
mp = head;
head = mp->next;
}
pthread_mutex_unlock(&lock);
printf("Consume %d\n", mp->num);
free(mp);
sleep(rand() % 5);
}
}
void *producer(void *p)
{
struct msg *mp;
for (;;)
{
mp = malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1;
printf("Produce %d \n", mp->num);
pthread_mutex_lock(&lock);
mp->next = head;
head = mp;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_product);
sleep(rand() % 5);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid;
srand(time(NULL));
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
1.1.1 3.3. Semaphore
Mutex變數是非0即1的,可看作一種資源的可用數量,初始化時Mutex是1,表示有一個可用資源,加鎖時獲得該資源,將Mutex減到0,表示不再有可用資源,解鎖時釋放該資源,將Mutex重新加到1,表示又有了一個可用資源。
信號量(Semaphore)和Mutex類似,表示可用資源的數量,和Mutex不同的是這個數量可以大於1。
本節介紹的是POSIX semaphore庫函數,詳見sem_overview(7),這種信號量不僅可用於同一進程的執行緒間同步,也可用於不同進程間的同步。
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t * sem);
int sem_destroy(sem_t * sem);
semaphore變數的類型為sem_t,
sem_init()初始化一個semaphore變數,value參數表示可用資源的數量,pshared參數為0表示信號量用於同一進程的執行緒間同步,本節只介紹這種情況。
在用完semaphore變數之後應該調用sem_destroy()釋放與semaphore相關的資源。
調用sem_wait()可以獲得資源,使semaphore的值減1,
如果調用sem_wait()時semaphore的值已經是0,則掛起等待。
如果不希望掛起等待,可以調用sem_trywait()。
調用sem_post()可以釋放資源,使semaphore的值加1,同時喚醒掛起等待的執行緒。
上一節生產者-消費者的例子是基於鏈表的,其空間可以動態分配,現在基於固定大小的環形佇列重寫這個程式:
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>
#define NUM 5
int queue[NUM];
sem_t blank_number, product_number;
void *producer(void *arg)
{
int p = 0;
while (1) {
sem_wait(&blank_number);
queue[p] = rand() % 1000 + 1;
printf("Produce %d\n", queue[p]);
sem_post(&product_number);
p = (p+1)%NUM;
sleep(rand()%5);
}
}
void *consumer(void *arg)
{
int c = 0;
while (1) {
sem_wait(&product_number);
printf("Consume %d\n", queue[c]);
queue[c] = 0;
sem_post(&blank_number);
c = (c+1)%NUM;
sleep(rand()%5);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid;
sem_init(&blank_number, 0, NUM);
sem_init(&product_number, 0, 0);
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
sem_destroy(&blank_number);
sem_destroy(&product_number);
return 0;
}
1.1.1.1 習題
1、本節和上一節的例子給出一個重要的提示:
用Condition Variable可以實現Semaphore。
請用Condition Variable實現Semaphore,然後用自己實現的Semaphore重寫本節的程式。
1.1.2 3.4. 其它執行緒間同步機制
如果共用資料是唯讀的,那麼各執行緒讀到的資料應該總是一致的,不會出現訪問衝突。只要有一個執行緒可以改寫資料,就必須考慮執行緒間同步的問題。由此引出了讀者寫者鎖(Reader-Writer Lock)的概念,Reader之間並不互斥,可以同時讀共用資料,而Writer是獨佔的(exclusive),在Writer修改資料時其它Reader或Writer不能訪問資料,可見Reader-Writer Lock比Mutex具有更好的併發性。
用掛起等待的方式解決訪問衝突不見得是最好的辦法,因為這樣畢竟會影響系統的併發性,在某些情況下解決訪問衝突的問題可以儘量避免掛起某個執行緒,例如Linux內核的Seqlock、RCU(read-copy-update)等機制。
關於這些同步機制的細節,有興趣的讀者可以參考[APUE2e]和[ULK]。
Posix執行緒(IBM)
http://www.ibm.com/developerworks/cn/linux/theme/posix_thread/index.html
POSIX 執行緒詳解、POSIX 執行緒程式設計、NPTL
Linux 執行緒模型
POSIX 執行緒詳解
Posix 執行緒程式設計指南
POSIX 表示可移植作業系統介面(Portable Operating System Interface ,縮寫為 POSIX 是為了讀音更像 UNIX)。電氣和電子工程師協會(Institute of Electrical and Electronics Engineers,IEEE)最初開發 POSIX 標準,是為了提高 UNIX 環境下應用程式的可攜性。具體的說 POSIX 是 IEEE 為要在各種 UNIX 作業系統上運行的軟體定義 API 所規定的一系列互相關聯的標準的總稱,而 X 則表明其對 Unix API 的傳承。Linux 基本上逐步實現了 POSIX 相容,但並沒有參加正式的 POSIX 認證。當前的 POSIX 文檔分為三個部分:POSIX Kernel API,POSIX 命令和工具集,及 POSIX 一致性測試。Posix 執行緒(POSIX threads,又稱 Pthreads)是負責 POSIX 的 IEEE 委員會開發的一套執行緒介面。
1.1 Linux 執行緒模型
Linux 最初用的執行緒模型是 LinuxThread, 它不相容 POSIX,而且存在一些性能問題,所以目前 Linux 摒棄了它,採用了基於 Pthreads 的 NPTL(Native POSIX Threads Library for Linux)模型, NPTL 修復了 LinuxThread 的許多缺點,並提供了更好的性能。
Linux 執行緒實現機制分析
Linux 執行緒模型的比較:LinuxThreads 和 NPTL
Linux 執行緒庫性能測試與分析
1.2 POSIX 執行緒詳解
Daniel Robbins 從實例入手,逐步講解 POSIX thread 程式設計技巧,有共用記憶體、互斥以及條件變數的運用。
pthreads 的基本用法 -- 介紹 POSIX 執行緒
POSIX 執行緒詳解,第 1 部分:一種支援記憶體共用的簡捷工具
POSIX 執行緒詳解,第 2 部分:稱作互斥對象的小玩意
POSIX 執行緒詳解,第 3 部分:使用條件變數提高效率
1.2.1 pthreads 的基本用法
1.2.1.1 介紹 POSIX 執行緒
Peter Seebach ([email protected]), 自由作者
簡介: 執行緒問題是令許多程式師頭痛的問題。UNIX 的進程模型簡單易懂,但有時效率低下。執行緒技術通常能使性能得到實質性的改進,付出的代價就是代碼有點混亂。本文揭開了 POSIX 執行緒介面的神秘面紗,並提供了執行緒化代碼的實際例子作為參考。
級別: 初級
除了 Anne McCaffrey 的系列小說 Dragonriders of Pern 之外,“執行緒”是令程式師談虎色變的詞兒。執行緒有時稱為輕型進程,是與大型複雜的項目相關的。調用庫函數時經常會遇到一些“執行緒不安全”的可怕警告。但這些執行緒究竟是什麼?使用它們能做什麼?使用執行緒有什麼風險?
本文通過一個簡單的執行緒應用程式來介紹執行緒。使用的執行緒模型是 POSIX 執行緒介面,通常稱為 pthreads。本文例子基於 SuSE Linux 8.2 平臺。所有代碼都在 SuSE Linux 8.2 上和 NetBSD-current 的最新構建上測試通過。
1.2.1.2 什麼是執行緒?
執行緒和進程十分相似,不同的只是執行緒比進程小。首先,執行緒採用了多個執行緒可共用資源的設計思想;例如,它們的操作大部分都是在同一位址空間進行的。其次,從一個執行緒切換到另一執行緒所花費的代價比進程低。再次,進程本身的資訊在記憶體中佔用的空間比執行緒大,因此執行緒更能允分地利用記憶體。
執行緒之間通常需要進行交互,因此就存在使用 IPC 進行多進程通信的問題。本文中對於多進程通信問題不做過多的討論,因為 POXIS 執行緒 API 提供了處理諸如鎖死和競態條件這類問題的工具。本文主要討論特定於多執行緒程式設計的問題和解決方案,一般的多道程序設計問題留待以後討論。
執行緒程式有時會出現在多進程和 IPC 程式設計中不常出現的一些問題。例如,如果兩個執行緒同時調用一個函數,如 asctime() (它使用一個靜態的資料區),會產生不可思議的結果。這是“執行緒安全”要考慮的問題。
1.2.1.3 一個簡單程式
本文使用的示例程式是一個投骰子程式。人們在玩角色扮演遊戲或者戰爭遊戲時,經常會把很多的時間花費在擲骰子上。一個可以通過網路存取的擲骰子程式在許多方面適合作為示例程式。其中程式碼非常簡單是最重要的原因,這樣對程式邏輯的理解就不會影響對執行緒的理解。
最使人分心的是網路部分,為了儘量簡化我們的學習過程,這部分代碼全都封裝在一些副程式中。第一個副程式 socket_setup() ,它返回一個準備接受連接的通訊端。第二個副程式 get_sockets() ,以這個通訊端為參數,接受連接,並創建一個 struct sockets物件。
struct sockets 物件只是一個對輸入/輸出埠的抽象描述。一個 FILE * 用於輸入,另一個用於輸出,還有一個標誌用來提醒我們以後正確地關閉通訊端。
您可以從 參考資料以 tar 壓縮格式下載該程式的不同版本,並在單獨的 shell 中查看或運行這些程式,或者在單獨的流覽器視窗中查看。第一個版本是 dthread1.c。
讓我們仔細分析一下這個程式 dthread1 。它具有幾個選項。第一個選項(當前未用,為功能擴展保留)是一個調試標誌,用選項 -d表示。第二個選項是 -s ,它決定程式是否在控制台環境運行。如果沒有指定 -s 選項,程式將會監聽連接並通過連接進行會話。第三個選項 -t 確定是否運行多執行緒。如果沒有指定 -t 選項,程式將只處理一個連接,處理完成後就退出。最後一個是 -S 選項,它使程式在每兩次投擲間停頓一秒。使用這個選項只是為了有趣,因為這樣我們就容易看到多個連接間的交替過程。
內核執行緒和用戶執行緒
在某些作業系統中, dthread1 可能會產生預期外的結果,原因在於它試圖去讀通訊端時阻塞了整個程式的運行,而不僅僅是單一執行緒的運行。這是內核執行緒和用戶執行緒間的一個區別所在。
使用者執行緒是一個精細的軟體工具,允許多執行緒的程式運行時不需要特定的內核支援。但這也有不利的一面,如果有執行緒進行系統調用並發生阻塞,整個進程就會阻塞。而內核執行緒允許一個執行緒被阻塞時,而其餘執行緒可以正常運行。
POSIX API 沒有限定執行緒應該如何工作,因此對於如何去編制執行緒程式就留有了很大的餘地。最新式的系統將具有內核執行緒。這裡對這個問題的講述過於簡單,如果您有興趣去深入瞭解,可以讀一讀您所用系統的 pthreads 庫的原始程式碼。
我們來好好研究一下這個程式。要該程式一運行就連接到它,請嘗試telnet localhost 6173 。如果以前您對這類擲骰子的遊戲不熟悉,那麼請 2d6 開始 。所支援的一般語法是“NdX”,意思是投 N 個骰子,每個骰子可以投出的範圍從 1 到 X(從代碼中可以看出,程式實際是投一個具有 X 面的骰子 N 次)。
這裡看到了執行緒程式最簡單的形式。有一點需要注意的是:每個執行緒都只是處理自己獨特的資料。(這並不完全正確;如果您發現了這個錯誤,說明您很有觀察力)。這樣可以避免各個執行緒相互交疊。為此,pthread_create() 將接受一個 void * 類型的參數,這個參數會被傳到執行緒開始執行的那個函數中去。這樣允許您創建一個任意複雜的資料結構,並將它作為一個指標傳送給需要在這個資料結構上進行操作的執行緒。當其位址傳入給 pthread_create() 的函數結束後,該執行緒也就結束了,而其他執行緒會繼續運行。
對於多執行緒程式來說一個顯而易見的問題是如何乾淨徹底地終止該程式(同樣,我們也可以簡單地用 Ctrl-C 來終止它)。對於單執行緒程式來說,我們很容易知道是如何終止的:當使用者退出時程式就退出了。但是對於連接了四個使用者的程式,應該何時退出呢?答案明顯是“在最後一個用戶退出後”。但是如何確定最後一個用戶已經退出呢?一種解決方法是增加一個變數,每創建一個新的執行緒該變數就加 1,每終止一個執行緒該變數就減 1,當該變數再次為零時,就關閉整個進程。
這個方法聽起來不錯,然而它同時也存在會使程式崩潰的危險。
1.2.1.4 競態條件(Race Condition)和互斥(Mutex)
可以想像一下,如果在一個執行緒正在創建的同時另一執行緒正在退出,那麼會發生什麼情況呢?如果執行緒調度器正巧在它們之間切換,程式會莫名其妙地關閉。執行緒 1 正在執行 i = i + 1; 這樣的代碼,執行緒 2 則在執行 i = i - 1; 這樣的代碼。為了討論的方便,假定變數 i 的初始值是 2。
執行緒 1:取出 i 的值(2)。
執行緒 1: i 加 1 (結果是 3)。
執行緒 2:取出 i 的值(2)。
執行緒 2:將 i 的值減 1 (結果是 1)。
執行緒 2:將結果保存到 i ( i=1)。
執行緒 1:將結果保存到 i ( i=3)。
啊呀!
我們這裡遇到的情況叫做競態條件(race condition),是一種出錯概率非常小的條件,意味著您只有非常快速或者非常運氣不好才會遇到這種情況。競態條件在幾百萬次運行中也很少遇到一次,所以很難調試出來。
我們需要採取一些方法避免執行緒 1 和執行緒 2 出現上述情況;這些方法要保證執行緒 1 “在完成對 i 的操作前不允許其他執行緒對 i 操作”。可以進行許多有趣的嘗試去發現一個合適的方法;這個方法要保證兩個執行緒不會衝突。您可以利用現有的機制自己編制代碼去嘗試,從中可以體會到更多的樂趣。
下一個要理解的概念就是 互斥(mutex)。互斥量(mutex 是 MUTual EXclusion 的縮寫)是避免執行緒間相互交疊的一種方法。可以把它想像成一個惟一的物體,必須把它收藏好,但是只有別人都不佔有它時您才可以佔有它,在您主動放棄它之前也沒有人可以佔有它。佔有這個惟一物體的過程就叫做鎖定或者獲得互斥量。不同的人學到的對這這件事的叫法不同,所以當您和別人談到這件事時別人可能會用不同的詞來表達。POSIX 執行緒介面沿用相當一致的術語,所以稱為“鎖定”。
創建和使用互斥量的過程比僅僅是開始一個執行緒的過程要稍微複雜一些。互斥量物件必須先被聲明;聲明後還必須初始化。做完這些之後,才可以被加鎖和解鎖。
1.2.1.5 互斥代碼:第一個例子
在流覽器(或者展開的 pth 目錄)中查看 dthread2.c。
為了方便,對 pthread_create() 的調用被單獨放到一個新的稱為 spawn() 的常式中。這樣做使增加互斥代碼時只需要在一個子程式中進行改動。這個常式做了一些新的工作;它鎖定一個叫做 count_mutex 的互斥量。之後它創建一個新的執行緒同時增量 threadcount;在完成之後,它解鎖互斥量。當一個執行緒準備終止時,它再次鎖定互斥量,減量 threadcount ,然後解鎖互斥量。如果互斥量threadcount 的值減小到了零,我們知道這時已經沒有執行緒在運行了,該退出程式了。然後這種說法並不完全正確;這時仍然有一個執行緒在運行,這個執行緒是進程初始運行時建立的執行緒。
您可能會注意到對一個叫做 pthread_mutex_init() 的函數的調用。這個函數對互斥量運行環境進行初始化,這個過程對互斥量正常工作是必需的。當不再使用互斥量時,可以調用 pthread_mutex_destroy() 來釋放在初始化過程中分配的資源。有些實現不分配或釋放任何資源;只是使用了 API。如果您不進行這些調用,總有您最不期望出現的那一刻,一個生產系統將會使用一種新的依賴於這些調用的實現 —— 您會在臨晨 3 點鐘發現它帶來的問題。
在 spawn() 中將鎖定互斥量的代碼緊跟在改變 threadcount 的後面似乎是合理的。聽起來也不錯,不幸的是,這樣做實際會引入一個令人氣餒的 bug。這種程式會非常頻繁地輸出一個提示,然後掛起不動。聽起來有些令人不解?記住,一旦 pthread_create() 被調用,新的執行緒就開始執行了。所以事件發生的順序看起來是以下面的順利進行的:
主執行緒:調用 pthread_create()。
新的子執行緒:輸出提示資訊。
舊的子執行緒:退出,減量 threadcount 到 0。
主執行緒:鎖定互斥量並增量 threadcount。
當然,按這個順序最後一步永遠不會執行。
如果您將調用 pthread_create() 前面改變 threadcount 值的代碼去掉,那麼互斥量代碼中間就只剩下減小計數值的語句了。示例程式不是按照這樣寫的,這樣做只是為了增加例子的趣味性。
1.2.1.6 解開鎖死
這篇文章的前面曾經提到過原始程式中的一個微妙的潛在的競態條件。現在,謎底已經揭穿。這個不易發現的競態條件是 rand() 存在內部狀態。如果在兩個執行緒交疊時調用 rand() ,它就可能返回一個錯誤的亂數。對這個程式來說,這可能不是什麼大問題,但對於一個以亂數的再現性為基礎的正規的類比程式來說,這可就是一個大問題了。
所以,讓我們更進一步,在產生亂數的地方加一個互斥量。這樣,我們就容易地解決了這個競態條件問題。
流覽 dthread3.c,或者在 pth 目錄下打開這個檔。
不幸的是,這裡仍舊存在另一個潛在的問題,叫做鎖死。當兩個(或以上)執行緒相互等待別的執行緒時就會出現鎖死。想像這兒有兩個互斥量,我們分別稱它們為 count_mutex 和 rand_mutex 。現在,有兩個執行緒需要使用這兩個互斥量。執行緒 1 的活動如下:
mutex_lock(&count_mutex);
mutex_lock(&rand_mutex);
mutex_unlock(&rand_mutex);
mutex_unlock(&count_mutex);
而執行緒 2 卻是以另外的循序執行這些語句:
mutex_lock(&rand_mutex);
mutex_lock(&count_mutex);
mutex_unlock(&count_mutex);
mutex_unlock(&rand_mutex);
這時就會發生鎖死等待。如果這兩個執行緒以上面的順序同時開始執行,它們同時開始執行鎖定:
Thread 1: mutex_lock(&count_mutex);
Thread 2: mutex_lock(&rand_mutex);
接下來會發生什麼?如果執行緒 1 要運行,它就要鎖定 rand_mutex ,可是這個互斥量已經被執行緒 2 阻塞了。如果執行緒 2 要運行,它就要鎖定 count_mutex ,而這個互斥量已經被執行緒 1 佔有了。這種情況可以引用一則杜撰的 Texas 法規來形容:“當兩列火車在十字路口相遇時,它們都會停止前進,都等待對方開走後才能前進”。
像這樣的問題,一個簡單的解決辦法是保證以相同的順序獲得互斥量。相似地,使每列火車安全通過的一個簡單辦法是始終對火車保持控制。在實際程式中,使引起鎖死的調用都整齊地排隊進行不太可能,即使在我們的簡單的示例程式中(如果正確安排調用的順序,完全可以避免鎖死),對互斥量的調用也不是完全相鄰的。
現在,將注意力轉到下一個例子, dthread4。
這個版本的程式演示了一個產生鎖死的一般來源:程式師的錯誤。
這個程式允許在一行上有多個規範,同時也限制骰子是角色遊戲中常見的普通的多面骰子。這個壞程式的開發者起初的想法是好的,即只在真正需要使用之前才鎖定這些互斥量,但是他卻直到運行結束才解鎖。因此,如果用戶輸入“2d6 2d8”,程式會鎖定六面的骰子,投擲兩次,然後鎖定八面的骰子,投擲兩次。只有等到所有的投擲過程全部結束,才將所有骰子解鎖。
與以前版本不同的是,這個版本很容易進入鎖死狀態。如果您願意的話,設想一下兩個用戶同時請求擲骰子,一個請求“2d6 2d8”,另一個請求“2d8 2d6”。會發生什麼情況呢?
執行緒 1:鎖定六面的骰子;投擲。
執行緒 2:鎖定八面的骰子;投擲。
執行緒 1:試圖鎖定八面的骰子。
執行緒 2:試圖鎖定六面的骰子。
“聰明的”解決方案實際上是根本沒有解決方案。如果投骰子的人只要一投完就馬上就釋放他擁有的骰子,就不會出現這個問題。
從中我們得到的第一個教訓是,您可以深究類比情況;坦白地說,鎖定單個骰子的存取愚蠢的。然而,第二個教訓是,不可能看到代碼裡面的鎖死。應該鎖定哪些互斥量取決於僅在運行時可用的資料,這是問題的關鍵所在。
如果您想實際體會一下這個 bug,試著使用 -S 選項,這樣程式運行時您就有足夠的時間在不同的終端間切換並且進行觀察。現在,您打算怎麼改正?為了討論的方便,假設有必要鎖定單個的骰子。您是怎麼做的呢? dthread5.c 中是一個天真的解決方案。到了這兒,您就可以回到前面給每個骰子加一個亂數產生器。那些好的遊戲者明白骰子投出來的結果好壞各半,您不會浪費擲出來的好點數,對吧?
鎖死也會發生在單獨的執行緒中。缺省使用的互斥量是“快速”的,這種互斥量有一個很大的優點,如果您試圖去鎖定一個您已經鎖定的互斥量,這時就會產生鎖死。如果可能,在設計程式時決不要鎖定一個已經鎖定的互斥量。另外,您還可以使用“遞迴”類型的互斥量,這種互斥量允許對同一個互斥量鎖定多次。還有一種互斥量是用於檢測一般錯誤的,如對一個已經解鎖的互斥量再次解鎖。注意,遞迴互斥量不能幫您解決程式中實際存在的鎖定 bug。下面的程式碼片段是從早一些版本的 dthread3.c 中摘出來的:
清單 1. 摘自 dthread3.c 的程式碼片段,其中包含 bug
int
roll_die(int n) {
pthread_mutex_lock(&rand_mutex);
return rand() % n + 1;
pthread_mutex_unlock(&rand_mutex);
}
看看您能不能比我更快發現 bug(我用了大約 5 分鐘)。為了守信,試著在早上 4:30 開始查錯。您可以在本文結尾處的 側欄中找到答案。
對鎖死的全面討論超出了本文的範圍,但現在您知道應該去查什麼資料了,而且可以在 參考資料一節的參考資料列表中找到更多的關於互斥量和鎖死的資料。
1.2.1.7 條件變數
條件變數是另一種有趣的變數,條件變數允許在條件不滿足時阻塞執行緒,等條件為真時再喚醒該執行緒。函數 pthread_cond_wait() 主要就是用於阻塞執行緒的,它有兩個參數;第一個是一個指向條件變數的指標,第二個是一個鎖定了的互斥量。條件變數同互斥量一樣必須使用 API 調用對其初始化,這個 API 調用就是 pthread_cond_init() 。當不再使用條件變數時,應該調用pthread_cond_destroy() 釋放它在初始化時分配的資源。對於互斥量來說,這些調用在有些實現中可能並不做什麼,但是您也應該調用它們。
當 pthread_cond_wait() 被調用後,它解鎖互斥量並停止執行緒的執行。在別的執行緒喚醒它之前它會一直保持暫停狀態。這些操作是“原子操作”;它們總是一起執行,沒有任何其他執行緒在它們之間執行。它們執行完之後,其他執行緒才開始運行。如果另一個執行緒對一個條件變數調用 pthread_cond_signal() ,那麼那個等待這個條件而被阻塞的執行緒就會被喚醒。如果另一個執行緒對這個條件變數調用pthread_cond_broadcast() ,那麼所有等待這個條件而被阻塞的執行緒都會被喚醒。
最後,當一個執行緒從調用 pthread_cond_wait() 而被喚醒時,要做的第一件事就是重新鎖定它在最初調用時解鎖的那個互斥量。這個操作要消耗一定的時間,實際上,這個時間太長了,可能在進行這項操作的同時,執行緒所等待的條件變數的值在這期間已經改變了。比如,一個正在等待貨物被加入到鏈表中的執行緒,當它被喚醒時可能發現鏈表是空的。在有些實現中,執行緒可能偶爾在沒有信號送到條件變數的情況下醒過來。執行緒程式設計不一定總是精確的科學,所以在程式設計時要始終注意這一點。
回頁首
1.2.1.8 更多內容
當然,以上內容僅僅是 POSIX 執行緒 API 的一點皮毛。有些使用者可能發現需要使用 pthread_join() 調用,這個函數可以使一個執行緒處於等候狀態,直到另一個執行緒執行完成。這裡有些可以設置的屬性,以便完成對調度的控制。在 Web 上有許多關於 POSIX 執行緒的參考資料。記得要閱讀使用手冊。您可以使用命令 apropos pthread 或 man -k pthread 得到與 pthread 相關的使用手冊。
練習題答案
清單 1中的這段代碼存在一個明顯、但是經常被忽略的 bug,您找到了嗎?
這個錯誤是,在互斥量解鎖前,返回語句就結束了程式的運行。
1.2.1.9 參考資料
您可以參閱本文在 developerWorks 全球網站上的 英文原文.
本文中用到的所有檔都可以作為一個 tar 文件 一次下載或者從以下連結單獨下載:
Makefile
dthread1.c
dthread2.c
dthread3.c
dthread4.c
dthread5.c
Daniel Robbins 在 developerWorks上一個由三部分組成的系列文章“ POSIX 執行緒詳解”中介紹了 POSIX 執行緒 API。
Open Directory 上有一個 POSIX 執行緒專欄,羅列了許多庫、教程和 FAQ。
IBM AIX 資源 POSIX 執行緒 API—— 都摘自 IBM eServer Solutions,這些都是非常有用的資料。
關於 AIX 的書籍 General programming concepts: Writing and debugging programs 中的兩個章節“ Understanding threads and processes”和“ Multi-threaded programming”中的內容適用於其他平臺上的執行緒程式設計。
GNU Portable Threads,或者稱為 GNU Pth,提供了一個與 POSIX 相容的執行緒的免費軟體實現,而 GNU 軟體的 操作手冊一向是非常好的。
“ 走向 Linux 2.6”討論了內核搶佔、futexes、新的調度程式以及更多的內容最新變化( developerWorks,2003 年 11 月)。
在“ 管理進程和執行緒”( developerWorks,2002 年 2 月)一文中,Ed Bradford 集中講述了 Linux 和 Windows 環境中的執行緒和進程。
要瞭解更關於 POSIX 執行緒的內容,包括連接和調度執行緒,請閱讀 Lawrence Livermore National Laboratories 的線上教程POSIX threads programming和 Little Unix Programmers Group (LUPG) 的 Multi-threaded programming with POSIX threads。
這兒有一本 理解互斥量和鎖的指南,它雖然不是基於 POSIX 執行緒進行講解的,但是所涉及的問題具有通用性。
關於 pthread_create 、 pthread_join 、 pthread_mutex_init 和 pthread_cond_init 的手冊頁是非常有用的。您可以在終端視窗中查看它們,或者從 Linux Man Pages Online找到它們。
Peter 在 SUSE Linux和 NetBSD上測試了他編寫的擲骰子程式。
並不是所有人都熱衷於執行緒。在 1996 年與 Usenix 的一次談話中,Tcl 語言的發明者 John Ousterhout (現在供職於 Electric Cloud, Inc,那時在 Sun 公司工作)認為用事件比用執行緒更好,可以通過他在 貝爾實驗室的個人頁面,或者 他 1996 年的幻燈片(PDF 格式)來瞭解他的觀點。
Anne McCaffrey 是一位作家,他喜歡寫 Pern 星球上和龍相關的神話故事。在 Pern 星球上有一道可怕的“銀線(silver threads)”忽然降落,讓那些關心的人感到恐慌。要問 Anne 的一個問題是:這些從地球來的殖民者,他們一定要乘坐某種飛船飛越很長的一段距離到 Pern 星球去進行殖民統治 —— 到第二本書他們還沒有發明 望遠鏡?
網路擲骰子程式在角色扮演類遊戲(即 RPG)中非常有用,您從來沒有玩過這類遊戲嗎?可以通過 Paul Elliot 的 什麼是角色扮演?或者 Dave 的 角色扮演介紹瞭解更多的內容。如果您確信已經明白,可以到 Nethack得到 Amulet of Yendor。
1.2.2 POSIX 執行緒詳解,第 1 部分:一種支援記憶體共用的簡捷工具
1.2.3 POSIX 執行緒詳解,第 2 部分:稱作互斥對象的小玩意
1.2.4 POSIX 執行緒詳解,第 3 部分:使用條件變數提高效率
1.3 Posix 執行緒程式設計指南
本系列文章在闡明概念的基礎上,向您詳細地講述了 Posix 執行緒庫 API。
Posix 執行緒程式設計指南,第 1 部分:執行緒創建與取消
Posix 執行緒程式設計指南,第 2 部分:執行緒私有數據
Posix 執行緒程式設計指南,第 3 部分:執行緒同步
Posix 執行緒程式設計指南,第 4 部分:執行緒終止
Posix 執行緒程式設計指南,第 5 部分:雜項
POSIX 執行緒詳解、POSIX 執行緒程式設計、NPTL
Linux 執行緒模型
POSIX 執行緒詳解
Posix 執行緒程式設計指南
POSIX 表示可移植作業系統介面(Portable Operating System Interface ,縮寫為 POSIX 是為了讀音更像 UNIX)。電氣和電子工程師協會(Institute of Electrical and Electronics Engineers,IEEE)最初開發 POSIX 標準,是為了提高 UNIX 環境下應用程式的可攜性。具體的說 POSIX 是 IEEE 為要在各種 UNIX 作業系統上運行的軟體定義 API 所規定的一系列互相關聯的標準的總稱,而 X 則表明其對 Unix API 的傳承。Linux 基本上逐步實現了 POSIX 相容,但並沒有參加正式的 POSIX 認證。當前的 POSIX 文檔分為三個部分:POSIX Kernel API,POSIX 命令和工具集,及 POSIX 一致性測試。Posix 執行緒(POSIX threads,又稱 Pthreads)是負責 POSIX 的 IEEE 委員會開發的一套執行緒介面。
1.1 Linux 執行緒模型
Linux 最初用的執行緒模型是 LinuxThread, 它不相容 POSIX,而且存在一些性能問題,所以目前 Linux 摒棄了它,採用了基於 Pthreads 的 NPTL(Native POSIX Threads Library for Linux)模型, NPTL 修復了 LinuxThread 的許多缺點,並提供了更好的性能。
Linux 執行緒實現機制分析
Linux 執行緒模型的比較:LinuxThreads 和 NPTL
Linux 執行緒庫性能測試與分析
1.2 POSIX 執行緒詳解
Daniel Robbins 從實例入手,逐步講解 POSIX thread 程式設計技巧,有共用記憶體、互斥以及條件變數的運用。
pthreads 的基本用法 -- 介紹 POSIX 執行緒
POSIX 執行緒詳解,第 1 部分:一種支援記憶體共用的簡捷工具
POSIX 執行緒詳解,第 2 部分:稱作互斥對象的小玩意
POSIX 執行緒詳解,第 3 部分:使用條件變數提高效率
1.2.1 pthreads 的基本用法
1.2.1.1 介紹 POSIX 執行緒
Peter Seebach ([email protected]), 自由作者
簡介: 執行緒問題是令許多程式師頭痛的問題。UNIX 的進程模型簡單易懂,但有時效率低下。執行緒技術通常能使性能得到實質性的改進,付出的代價就是代碼有點混亂。本文揭開了 POSIX 執行緒介面的神秘面紗,並提供了執行緒化代碼的實際例子作為參考。
級別: 初級
除了 Anne McCaffrey 的系列小說 Dragonriders of Pern 之外,“執行緒”是令程式師談虎色變的詞兒。執行緒有時稱為輕型進程,是與大型複雜的項目相關的。調用庫函數時經常會遇到一些“執行緒不安全”的可怕警告。但這些執行緒究竟是什麼?使用它們能做什麼?使用執行緒有什麼風險?
本文通過一個簡單的執行緒應用程式來介紹執行緒。使用的執行緒模型是 POSIX 執行緒介面,通常稱為 pthreads。本文例子基於 SuSE Linux 8.2 平臺。所有代碼都在 SuSE Linux 8.2 上和 NetBSD-current 的最新構建上測試通過。
1.2.1.2 什麼是執行緒?
執行緒和進程十分相似,不同的只是執行緒比進程小。首先,執行緒採用了多個執行緒可共用資源的設計思想;例如,它們的操作大部分都是在同一位址空間進行的。其次,從一個執行緒切換到另一執行緒所花費的代價比進程低。再次,進程本身的資訊在記憶體中佔用的空間比執行緒大,因此執行緒更能允分地利用記憶體。
執行緒之間通常需要進行交互,因此就存在使用 IPC 進行多進程通信的問題。本文中對於多進程通信問題不做過多的討論,因為 POXIS 執行緒 API 提供了處理諸如鎖死和競態條件這類問題的工具。本文主要討論特定於多執行緒程式設計的問題和解決方案,一般的多道程序設計問題留待以後討論。
執行緒程式有時會出現在多進程和 IPC 程式設計中不常出現的一些問題。例如,如果兩個執行緒同時調用一個函數,如 asctime() (它使用一個靜態的資料區),會產生不可思議的結果。這是“執行緒安全”要考慮的問題。
1.2.1.3 一個簡單程式
本文使用的示例程式是一個投骰子程式。人們在玩角色扮演遊戲或者戰爭遊戲時,經常會把很多的時間花費在擲骰子上。一個可以通過網路存取的擲骰子程式在許多方面適合作為示例程式。其中程式碼非常簡單是最重要的原因,這樣對程式邏輯的理解就不會影響對執行緒的理解。
最使人分心的是網路部分,為了儘量簡化我們的學習過程,這部分代碼全都封裝在一些副程式中。第一個副程式 socket_setup() ,它返回一個準備接受連接的通訊端。第二個副程式 get_sockets() ,以這個通訊端為參數,接受連接,並創建一個 struct sockets物件。
struct sockets 物件只是一個對輸入/輸出埠的抽象描述。一個 FILE * 用於輸入,另一個用於輸出,還有一個標誌用來提醒我們以後正確地關閉通訊端。
您可以從 參考資料以 tar 壓縮格式下載該程式的不同版本,並在單獨的 shell 中查看或運行這些程式,或者在單獨的流覽器視窗中查看。第一個版本是 dthread1.c。
讓我們仔細分析一下這個程式 dthread1 。它具有幾個選項。第一個選項(當前未用,為功能擴展保留)是一個調試標誌,用選項 -d表示。第二個選項是 -s ,它決定程式是否在控制台環境運行。如果沒有指定 -s 選項,程式將會監聽連接並通過連接進行會話。第三個選項 -t 確定是否運行多執行緒。如果沒有指定 -t 選項,程式將只處理一個連接,處理完成後就退出。最後一個是 -S 選項,它使程式在每兩次投擲間停頓一秒。使用這個選項只是為了有趣,因為這樣我們就容易看到多個連接間的交替過程。
內核執行緒和用戶執行緒
在某些作業系統中, dthread1 可能會產生預期外的結果,原因在於它試圖去讀通訊端時阻塞了整個程式的運行,而不僅僅是單一執行緒的運行。這是內核執行緒和用戶執行緒間的一個區別所在。
使用者執行緒是一個精細的軟體工具,允許多執行緒的程式運行時不需要特定的內核支援。但這也有不利的一面,如果有執行緒進行系統調用並發生阻塞,整個進程就會阻塞。而內核執行緒允許一個執行緒被阻塞時,而其餘執行緒可以正常運行。
POSIX API 沒有限定執行緒應該如何工作,因此對於如何去編制執行緒程式就留有了很大的餘地。最新式的系統將具有內核執行緒。這裡對這個問題的講述過於簡單,如果您有興趣去深入瞭解,可以讀一讀您所用系統的 pthreads 庫的原始程式碼。
我們來好好研究一下這個程式。要該程式一運行就連接到它,請嘗試telnet localhost 6173 。如果以前您對這類擲骰子的遊戲不熟悉,那麼請 2d6 開始 。所支援的一般語法是“NdX”,意思是投 N 個骰子,每個骰子可以投出的範圍從 1 到 X(從代碼中可以看出,程式實際是投一個具有 X 面的骰子 N 次)。
這裡看到了執行緒程式最簡單的形式。有一點需要注意的是:每個執行緒都只是處理自己獨特的資料。(這並不完全正確;如果您發現了這個錯誤,說明您很有觀察力)。這樣可以避免各個執行緒相互交疊。為此,pthread_create() 將接受一個 void * 類型的參數,這個參數會被傳到執行緒開始執行的那個函數中去。這樣允許您創建一個任意複雜的資料結構,並將它作為一個指標傳送給需要在這個資料結構上進行操作的執行緒。當其位址傳入給 pthread_create() 的函數結束後,該執行緒也就結束了,而其他執行緒會繼續運行。
對於多執行緒程式來說一個顯而易見的問題是如何乾淨徹底地終止該程式(同樣,我們也可以簡單地用 Ctrl-C 來終止它)。對於單執行緒程式來說,我們很容易知道是如何終止的:當使用者退出時程式就退出了。但是對於連接了四個使用者的程式,應該何時退出呢?答案明顯是“在最後一個用戶退出後”。但是如何確定最後一個用戶已經退出呢?一種解決方法是增加一個變數,每創建一個新的執行緒該變數就加 1,每終止一個執行緒該變數就減 1,當該變數再次為零時,就關閉整個進程。
這個方法聽起來不錯,然而它同時也存在會使程式崩潰的危險。
1.2.1.4 競態條件(Race Condition)和互斥(Mutex)
可以想像一下,如果在一個執行緒正在創建的同時另一執行緒正在退出,那麼會發生什麼情況呢?如果執行緒調度器正巧在它們之間切換,程式會莫名其妙地關閉。執行緒 1 正在執行 i = i + 1; 這樣的代碼,執行緒 2 則在執行 i = i - 1; 這樣的代碼。為了討論的方便,假定變數 i 的初始值是 2。
執行緒 1:取出 i 的值(2)。
執行緒 1: i 加 1 (結果是 3)。
執行緒 2:取出 i 的值(2)。
執行緒 2:將 i 的值減 1 (結果是 1)。
執行緒 2:將結果保存到 i ( i=1)。
執行緒 1:將結果保存到 i ( i=3)。
啊呀!
我們這裡遇到的情況叫做競態條件(race condition),是一種出錯概率非常小的條件,意味著您只有非常快速或者非常運氣不好才會遇到這種情況。競態條件在幾百萬次運行中也很少遇到一次,所以很難調試出來。
我們需要採取一些方法避免執行緒 1 和執行緒 2 出現上述情況;這些方法要保證執行緒 1 “在完成對 i 的操作前不允許其他執行緒對 i 操作”。可以進行許多有趣的嘗試去發現一個合適的方法;這個方法要保證兩個執行緒不會衝突。您可以利用現有的機制自己編制代碼去嘗試,從中可以體會到更多的樂趣。
下一個要理解的概念就是 互斥(mutex)。互斥量(mutex 是 MUTual EXclusion 的縮寫)是避免執行緒間相互交疊的一種方法。可以把它想像成一個惟一的物體,必須把它收藏好,但是只有別人都不佔有它時您才可以佔有它,在您主動放棄它之前也沒有人可以佔有它。佔有這個惟一物體的過程就叫做鎖定或者獲得互斥量。不同的人學到的對這這件事的叫法不同,所以當您和別人談到這件事時別人可能會用不同的詞來表達。POSIX 執行緒介面沿用相當一致的術語,所以稱為“鎖定”。
創建和使用互斥量的過程比僅僅是開始一個執行緒的過程要稍微複雜一些。互斥量物件必須先被聲明;聲明後還必須初始化。做完這些之後,才可以被加鎖和解鎖。
1.2.1.5 互斥代碼:第一個例子
在流覽器(或者展開的 pth 目錄)中查看 dthread2.c。
為了方便,對 pthread_create() 的調用被單獨放到一個新的稱為 spawn() 的常式中。這樣做使增加互斥代碼時只需要在一個子程式中進行改動。這個常式做了一些新的工作;它鎖定一個叫做 count_mutex 的互斥量。之後它創建一個新的執行緒同時增量 threadcount;在完成之後,它解鎖互斥量。當一個執行緒準備終止時,它再次鎖定互斥量,減量 threadcount ,然後解鎖互斥量。如果互斥量threadcount 的值減小到了零,我們知道這時已經沒有執行緒在運行了,該退出程式了。然後這種說法並不完全正確;這時仍然有一個執行緒在運行,這個執行緒是進程初始運行時建立的執行緒。
您可能會注意到對一個叫做 pthread_mutex_init() 的函數的調用。這個函數對互斥量運行環境進行初始化,這個過程對互斥量正常工作是必需的。當不再使用互斥量時,可以調用 pthread_mutex_destroy() 來釋放在初始化過程中分配的資源。有些實現不分配或釋放任何資源;只是使用了 API。如果您不進行這些調用,總有您最不期望出現的那一刻,一個生產系統將會使用一種新的依賴於這些調用的實現 —— 您會在臨晨 3 點鐘發現它帶來的問題。
在 spawn() 中將鎖定互斥量的代碼緊跟在改變 threadcount 的後面似乎是合理的。聽起來也不錯,不幸的是,這樣做實際會引入一個令人氣餒的 bug。這種程式會非常頻繁地輸出一個提示,然後掛起不動。聽起來有些令人不解?記住,一旦 pthread_create() 被調用,新的執行緒就開始執行了。所以事件發生的順序看起來是以下面的順利進行的:
主執行緒:調用 pthread_create()。
新的子執行緒:輸出提示資訊。
舊的子執行緒:退出,減量 threadcount 到 0。
主執行緒:鎖定互斥量並增量 threadcount。
當然,按這個順序最後一步永遠不會執行。
如果您將調用 pthread_create() 前面改變 threadcount 值的代碼去掉,那麼互斥量代碼中間就只剩下減小計數值的語句了。示例程式不是按照這樣寫的,這樣做只是為了增加例子的趣味性。
1.2.1.6 解開鎖死
這篇文章的前面曾經提到過原始程式中的一個微妙的潛在的競態條件。現在,謎底已經揭穿。這個不易發現的競態條件是 rand() 存在內部狀態。如果在兩個執行緒交疊時調用 rand() ,它就可能返回一個錯誤的亂數。對這個程式來說,這可能不是什麼大問題,但對於一個以亂數的再現性為基礎的正規的類比程式來說,這可就是一個大問題了。
所以,讓我們更進一步,在產生亂數的地方加一個互斥量。這樣,我們就容易地解決了這個競態條件問題。
流覽 dthread3.c,或者在 pth 目錄下打開這個檔。
不幸的是,這裡仍舊存在另一個潛在的問題,叫做鎖死。當兩個(或以上)執行緒相互等待別的執行緒時就會出現鎖死。想像這兒有兩個互斥量,我們分別稱它們為 count_mutex 和 rand_mutex 。現在,有兩個執行緒需要使用這兩個互斥量。執行緒 1 的活動如下:
mutex_lock(&count_mutex);
mutex_lock(&rand_mutex);
mutex_unlock(&rand_mutex);
mutex_unlock(&count_mutex);
而執行緒 2 卻是以另外的循序執行這些語句:
mutex_lock(&rand_mutex);
mutex_lock(&count_mutex);
mutex_unlock(&count_mutex);
mutex_unlock(&rand_mutex);
這時就會發生鎖死等待。如果這兩個執行緒以上面的順序同時開始執行,它們同時開始執行鎖定:
Thread 1: mutex_lock(&count_mutex);
Thread 2: mutex_lock(&rand_mutex);
接下來會發生什麼?如果執行緒 1 要運行,它就要鎖定 rand_mutex ,可是這個互斥量已經被執行緒 2 阻塞了。如果執行緒 2 要運行,它就要鎖定 count_mutex ,而這個互斥量已經被執行緒 1 佔有了。這種情況可以引用一則杜撰的 Texas 法規來形容:“當兩列火車在十字路口相遇時,它們都會停止前進,都等待對方開走後才能前進”。
像這樣的問題,一個簡單的解決辦法是保證以相同的順序獲得互斥量。相似地,使每列火車安全通過的一個簡單辦法是始終對火車保持控制。在實際程式中,使引起鎖死的調用都整齊地排隊進行不太可能,即使在我們的簡單的示例程式中(如果正確安排調用的順序,完全可以避免鎖死),對互斥量的調用也不是完全相鄰的。
現在,將注意力轉到下一個例子, dthread4。
這個版本的程式演示了一個產生鎖死的一般來源:程式師的錯誤。
這個程式允許在一行上有多個規範,同時也限制骰子是角色遊戲中常見的普通的多面骰子。這個壞程式的開發者起初的想法是好的,即只在真正需要使用之前才鎖定這些互斥量,但是他卻直到運行結束才解鎖。因此,如果用戶輸入“2d6 2d8”,程式會鎖定六面的骰子,投擲兩次,然後鎖定八面的骰子,投擲兩次。只有等到所有的投擲過程全部結束,才將所有骰子解鎖。
與以前版本不同的是,這個版本很容易進入鎖死狀態。如果您願意的話,設想一下兩個用戶同時請求擲骰子,一個請求“2d6 2d8”,另一個請求“2d8 2d6”。會發生什麼情況呢?
執行緒 1:鎖定六面的骰子;投擲。
執行緒 2:鎖定八面的骰子;投擲。
執行緒 1:試圖鎖定八面的骰子。
執行緒 2:試圖鎖定六面的骰子。
“聰明的”解決方案實際上是根本沒有解決方案。如果投骰子的人只要一投完就馬上就釋放他擁有的骰子,就不會出現這個問題。
從中我們得到的第一個教訓是,您可以深究類比情況;坦白地說,鎖定單個骰子的存取愚蠢的。然而,第二個教訓是,不可能看到代碼裡面的鎖死。應該鎖定哪些互斥量取決於僅在運行時可用的資料,這是問題的關鍵所在。
如果您想實際體會一下這個 bug,試著使用 -S 選項,這樣程式運行時您就有足夠的時間在不同的終端間切換並且進行觀察。現在,您打算怎麼改正?為了討論的方便,假設有必要鎖定單個的骰子。您是怎麼做的呢? dthread5.c 中是一個天真的解決方案。到了這兒,您就可以回到前面給每個骰子加一個亂數產生器。那些好的遊戲者明白骰子投出來的結果好壞各半,您不會浪費擲出來的好點數,對吧?
鎖死也會發生在單獨的執行緒中。缺省使用的互斥量是“快速”的,這種互斥量有一個很大的優點,如果您試圖去鎖定一個您已經鎖定的互斥量,這時就會產生鎖死。如果可能,在設計程式時決不要鎖定一個已經鎖定的互斥量。另外,您還可以使用“遞迴”類型的互斥量,這種互斥量允許對同一個互斥量鎖定多次。還有一種互斥量是用於檢測一般錯誤的,如對一個已經解鎖的互斥量再次解鎖。注意,遞迴互斥量不能幫您解決程式中實際存在的鎖定 bug。下面的程式碼片段是從早一些版本的 dthread3.c 中摘出來的:
清單 1. 摘自 dthread3.c 的程式碼片段,其中包含 bug
int
roll_die(int n) {
pthread_mutex_lock(&rand_mutex);
return rand() % n + 1;
pthread_mutex_unlock(&rand_mutex);
}
看看您能不能比我更快發現 bug(我用了大約 5 分鐘)。為了守信,試著在早上 4:30 開始查錯。您可以在本文結尾處的 側欄中找到答案。
對鎖死的全面討論超出了本文的範圍,但現在您知道應該去查什麼資料了,而且可以在 參考資料一節的參考資料列表中找到更多的關於互斥量和鎖死的資料。
1.2.1.7 條件變數
條件變數是另一種有趣的變數,條件變數允許在條件不滿足時阻塞執行緒,等條件為真時再喚醒該執行緒。函數 pthread_cond_wait() 主要就是用於阻塞執行緒的,它有兩個參數;第一個是一個指向條件變數的指標,第二個是一個鎖定了的互斥量。條件變數同互斥量一樣必須使用 API 調用對其初始化,這個 API 調用就是 pthread_cond_init() 。當不再使用條件變數時,應該調用pthread_cond_destroy() 釋放它在初始化時分配的資源。對於互斥量來說,這些調用在有些實現中可能並不做什麼,但是您也應該調用它們。
當 pthread_cond_wait() 被調用後,它解鎖互斥量並停止執行緒的執行。在別的執行緒喚醒它之前它會一直保持暫停狀態。這些操作是“原子操作”;它們總是一起執行,沒有任何其他執行緒在它們之間執行。它們執行完之後,其他執行緒才開始運行。如果另一個執行緒對一個條件變數調用 pthread_cond_signal() ,那麼那個等待這個條件而被阻塞的執行緒就會被喚醒。如果另一個執行緒對這個條件變數調用pthread_cond_broadcast() ,那麼所有等待這個條件而被阻塞的執行緒都會被喚醒。
最後,當一個執行緒從調用 pthread_cond_wait() 而被喚醒時,要做的第一件事就是重新鎖定它在最初調用時解鎖的那個互斥量。這個操作要消耗一定的時間,實際上,這個時間太長了,可能在進行這項操作的同時,執行緒所等待的條件變數的值在這期間已經改變了。比如,一個正在等待貨物被加入到鏈表中的執行緒,當它被喚醒時可能發現鏈表是空的。在有些實現中,執行緒可能偶爾在沒有信號送到條件變數的情況下醒過來。執行緒程式設計不一定總是精確的科學,所以在程式設計時要始終注意這一點。
回頁首
1.2.1.8 更多內容
當然,以上內容僅僅是 POSIX 執行緒 API 的一點皮毛。有些使用者可能發現需要使用 pthread_join() 調用,這個函數可以使一個執行緒處於等候狀態,直到另一個執行緒執行完成。這裡有些可以設置的屬性,以便完成對調度的控制。在 Web 上有許多關於 POSIX 執行緒的參考資料。記得要閱讀使用手冊。您可以使用命令 apropos pthread 或 man -k pthread 得到與 pthread 相關的使用手冊。
練習題答案
清單 1中的這段代碼存在一個明顯、但是經常被忽略的 bug,您找到了嗎?
這個錯誤是,在互斥量解鎖前,返回語句就結束了程式的運行。
1.2.1.9 參考資料
您可以參閱本文在 developerWorks 全球網站上的 英文原文.
本文中用到的所有檔都可以作為一個 tar 文件 一次下載或者從以下連結單獨下載:
Makefile
dthread1.c
dthread2.c
dthread3.c
dthread4.c
dthread5.c
Daniel Robbins 在 developerWorks上一個由三部分組成的系列文章“ POSIX 執行緒詳解”中介紹了 POSIX 執行緒 API。
Open Directory 上有一個 POSIX 執行緒專欄,羅列了許多庫、教程和 FAQ。
IBM AIX 資源 POSIX 執行緒 API—— 都摘自 IBM eServer Solutions,這些都是非常有用的資料。
關於 AIX 的書籍 General programming concepts: Writing and debugging programs 中的兩個章節“ Understanding threads and processes”和“ Multi-threaded programming”中的內容適用於其他平臺上的執行緒程式設計。
GNU Portable Threads,或者稱為 GNU Pth,提供了一個與 POSIX 相容的執行緒的免費軟體實現,而 GNU 軟體的 操作手冊一向是非常好的。
“ 走向 Linux 2.6”討論了內核搶佔、futexes、新的調度程式以及更多的內容最新變化( developerWorks,2003 年 11 月)。
在“ 管理進程和執行緒”( developerWorks,2002 年 2 月)一文中,Ed Bradford 集中講述了 Linux 和 Windows 環境中的執行緒和進程。
要瞭解更關於 POSIX 執行緒的內容,包括連接和調度執行緒,請閱讀 Lawrence Livermore National Laboratories 的線上教程POSIX threads programming和 Little Unix Programmers Group (LUPG) 的 Multi-threaded programming with POSIX threads。
這兒有一本 理解互斥量和鎖的指南,它雖然不是基於 POSIX 執行緒進行講解的,但是所涉及的問題具有通用性。
關於 pthread_create 、 pthread_join 、 pthread_mutex_init 和 pthread_cond_init 的手冊頁是非常有用的。您可以在終端視窗中查看它們,或者從 Linux Man Pages Online找到它們。
Peter 在 SUSE Linux和 NetBSD上測試了他編寫的擲骰子程式。
並不是所有人都熱衷於執行緒。在 1996 年與 Usenix 的一次談話中,Tcl 語言的發明者 John Ousterhout (現在供職於 Electric Cloud, Inc,那時在 Sun 公司工作)認為用事件比用執行緒更好,可以通過他在 貝爾實驗室的個人頁面,或者 他 1996 年的幻燈片(PDF 格式)來瞭解他的觀點。
Anne McCaffrey 是一位作家,他喜歡寫 Pern 星球上和龍相關的神話故事。在 Pern 星球上有一道可怕的“銀線(silver threads)”忽然降落,讓那些關心的人感到恐慌。要問 Anne 的一個問題是:這些從地球來的殖民者,他們一定要乘坐某種飛船飛越很長的一段距離到 Pern 星球去進行殖民統治 —— 到第二本書他們還沒有發明 望遠鏡?
網路擲骰子程式在角色扮演類遊戲(即 RPG)中非常有用,您從來沒有玩過這類遊戲嗎?可以通過 Paul Elliot 的 什麼是角色扮演?或者 Dave 的 角色扮演介紹瞭解更多的內容。如果您確信已經明白,可以到 Nethack得到 Amulet of Yendor。
1.2.2 POSIX 執行緒詳解,第 1 部分:一種支援記憶體共用的簡捷工具
1.2.3 POSIX 執行緒詳解,第 2 部分:稱作互斥對象的小玩意
1.2.4 POSIX 執行緒詳解,第 3 部分:使用條件變數提高效率
1.3 Posix 執行緒程式設計指南
本系列文章在闡明概念的基礎上,向您詳細地講述了 Posix 執行緒庫 API。
Posix 執行緒程式設計指南,第 1 部分:執行緒創建與取消
Posix 執行緒程式設計指南,第 2 部分:執行緒私有數據
Posix 執行緒程式設計指南,第 3 部分:執行緒同步
Posix 執行緒程式設計指南,第 4 部分:執行緒終止
Posix 執行緒程式設計指南,第 5 部分:雜項
1 Linux Multi-Thread Programming
https://sites.google.com/site/myembededlife/Home/applications--development/linux-multi-thread-programming
目錄
1 引言
2 簡單的多執行緒程式設計
3 修改執行緒的屬性
4 執行緒的資料處理
4.1 1) 執行緒數據
4.2 2) 互斥鎖
4.3 3) 條件變數
4.4 4) 信號量
4.5 5)非同步信號
4.6 6)關於執行緒的撤銷
1.1 引言
執行緒(thread)技術早在60年代就被提出,但真正應用多執行緒到作業系統中去,是在80年代中期,solaris是這方面的佼佼者。傳統的Unix也支持執行緒的概念,但是在一個行程(process)中只允許有一個執行緒,這樣多執行緒就意味著多行程(process)。現在,多執行緒技術已經被許多作業系統所支援,包括Windows/NT,當然,也包括Linux。
為什麼有了行程(process)的概念後,還要再引入執行緒呢?使用多執行緒到底有哪些好處?什麼的系統應該選用多執行緒?我們首先必須回答這些問題。
使用多執行緒的理由之一是和行程(process)相比,它是一種非常"節儉"的多工操作方式。我們知道,在Linux系統下,啟動一個新的行程(process)必須分配給它獨立的位址空間,建立眾多的資料表來維護它的程式碼片段、堆疊段和資料段,這是一種"昂貴"的多工工作方式。而運行於一個行程(process)中的多個執行緒,它們彼此之間使用相同的位址空間,共用大部分數據,啟動一個執行緒所花費的空間遠遠小於啟動一個行程(process)所花費的空間,而且,執行緒間彼此切換所需的時間也遠遠小於行程(process)間切換所需要的時間。據統計,總的說來,?0倍左右,當然,在具體的系統上,這個資料可能會有較大的區別。
使用多執行緒的理由之二是執行緒間方便的通信機制。對不同行程(process)來說,它們具有獨立的資料空間,要進行資料的傳遞只能通過通信的方式進行,這種方式不僅費時,而且很不方便。執行緒則不然,由於同一行程(process)下的執行緒之間共用資料空間,所以一個執行緒的資料可以直接為其它執行緒所用,這不僅快捷,而且方便。當然,資料的共用也帶來其他一些問題,有的變數不能同時被兩個執行緒所修改,有的副程式中聲明為static的資料更有可能給多執行緒程式帶來災難性的打擊,這些正是編寫多執行緒程式時最需要注意的地方。
除了以上所說的優點外,不和行程(process)比較,多執行緒程式作為一種多工、併發的工作方式,當然有以下的優點:
1) 提高應用程式回應。
這對圖形介面的程式尤其有意義,當一個操作耗時很長時,整個系統都會等待這個操作,此時程式不會回應鍵盤、滑鼠、功能表的操作,而使用多執行緒技術,將耗時長的操作(time consuming)置於一個新的執行緒,可以避免這種尷尬的情況。
2) 使多CPU系統更加有效。
作業系統會保證當執行緒數不大於CPU數目時,不同的執行緒運行於不同的CPU上。
3) 改善程式結構。
一個既長又複雜的行程(process)可以考慮分為多個執行緒,成為幾個獨立或半獨立的運行部分,這樣的程式會利於理解和修改。
下麵我們先來嘗試編寫一個簡單的多執行緒程式。
1.2 簡單的多執行緒程式設計
Linux系統下的多執行緒遵循POSIX執行緒介面,稱為pthread。
編寫Linux下的多執行緒程式,需要使用標頭檔pthread.h,連接時需要使用庫libpthread.a。
順便說一下,Linux下pthread的實現是通過系統調用clone( )來實現的。
clone( )是Linux所特有的系統調用,它的使用方式類似fork,關於clone( )的詳細情況,有興趣的讀者可以去查看有關文檔說明。
下面我們展示一個最簡單的多執行緒程式example1.c。
/* example.c*/
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h> // for exit() 更正
void thread(void)
{
int i;
for(i=0;i<3;i++)
printf("This is a pthread.\n");
}
int main(void)
{
pthread_t id;
int i,ret;
ret=pthread_create(&id,NULL,(void *) thread,NULL);
if(ret!=0){
printf ("Create pthread error!n");
exit (1);
}
for(i=0;i<3;i++)
printf("This is the main process.\n");
pthread_join(id,NULL);
return (0);
}
我們編譯此程式:
gcc example1.c -lpthread -o example1
運行example1,我們得到如下結果:
This is the main process.
This is a pthread.
This is the main process.
This is the main process.
This is a pthread.
This is a pthread.
再次運行,我們可能得到如下結果:
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
前後兩次結果不一樣,這是兩個執行緒爭奪CPU資源的結果。
上面的示例中,我們使用到了兩個函數,pthread_create和pthread_join,並聲明瞭一個pthread_t型的變數。
pthread_t在標頭檔/usr/include/bits/pthreadtypes.h中定義:
typedef unsigned long int pthread_t;
它是一個執行緒的識別字。函數pthread_create用來創建一個執行緒,它的原型為:
extern int pthread_create __P
(( pthread_t *__thread,
__const pthread_attr_t *__attr,
void *(*__start_routine) (void *),
void *__arg));
第一個參數為指向執行緒識別字的指標,
第二個參數用來設置執行緒屬性,
第三個參數是執行緒運行函數的起始位址,
最後一個參數是運行函數的參數。
這裡,我們的函數thread不需要參數,所以最後一個參數設為空指標(NULL)。第二個參數我們也設為空指標(NULL),這樣將生成預設屬性的執行緒。對執行緒屬性的設定和修改我們將在下一節闡述。
當創建執行緒成功時,函數返回0,若不為0則說明創建執行緒失敗,常見的錯誤返回代碼為EAGAIN和EINVAL。
前者(EAGAIN)表示系統限制創建新的執行緒,例如執行緒數目過多了;
後者(EINVAL)表示第二個參數代表的執行緒屬性值非法。
創建執行緒成功後,新創建的執行緒則運行參數三和參數四確定的函數,原來的執行緒則繼續運行下一行代碼。
函數pthread_join用來等待一個執行緒的結束。
函數原型為:
extern int pthread_join __P ((pthread_t __th, void **__thread_return));
第一個參數為被等待的執行緒識別字,
第二個參數為一個用戶定義的指標,它可以用來存儲被等待中的執行緒的返回值。
這個函數是一個執行緒阻塞的函數,調用它的函數將一直等待到被等待的執行緒結束為止,當函數返回時,被等待中的執行緒的資源被收回。
一個執行緒的結束有兩種途徑,一種是象我們上面的例子一樣,函數結束了,調用它的執行緒也就結束了;
另一種方式是通過函數pthread_exit來實現。它的函數原型為:
extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
唯一的參數是函數的返回代碼,只要pthread_join中的第二個參數thread_return不是NULL,這個值將被傳遞給thread_return。
最後要說明的是,一個執行緒不能被多個執行緒等待,否則第一個接收到信號的執行緒成功返回,其餘調用pthread_join的執行緒則返回錯誤代碼ESRCH。
在這一節裡,我們編寫了一個最簡單的執行緒,並掌握了最常用的三個函數pthread_create,pthread_join和pthread_exit。下面,我們來瞭解執行緒的一些常用屬性以及如何設置這些屬性。
1.3 修改執行緒的屬性
在上一節的例子裡,我們用pthread_create函數創建了一個執行緒,在這個執行緒中,我們使用了預設參數,即將該函數的第二個參數設為NULL。的確,對大多數程式來說,使用預設屬性就夠了,但我們還是有必要來瞭解一下執行緒的有關屬性。
屬性結構為pthread_attr_t,它同樣在標頭檔/usr/include/pthread.h中定義,喜歡追根問底的人可以自己去查看。屬性值不能直接設置,須使用相關函數進行操作,初始化的函數為pthread_attr_init,這個函數必須在pthread_create函數之前調用。
屬性物件主要包括是否綁定、是否分離、堆疊位址、堆疊大小、優先順序。
預設的屬性為非綁定、非分離、缺省1M的堆疊、與父進程同樣級別的優先順序。
關於執行緒的綁定,牽涉到另外一個概念:輕行程(LWP:Light Weight Process)。
輕行程可以理解為內核執行緒,它位於使用者層和系統層之間。系統對執行緒資源的分配、對執行緒的控制是通過輕行程來實現的,一個輕行程可以控制一個或多個執行緒。預設狀況下,啟動多少輕行程、哪些輕行程來控制哪些執行緒是由系統來控制的,這種狀況即稱為非綁定的。
綁定狀況下,則顧名思義,即某個執行緒固定的"綁"在一個輕行程之上。
被綁定的執行緒具有較高的回應速度,這是因為CPU時間片的調度是面向輕行程的,
綁定的執行緒可以保證在需要的時候它總有一個輕行程可用。
通過設置被綁定的輕行程的優先順序和調度級可以使得綁定的執行緒滿足諸如即時反應之類的要求。
設置執行緒綁定狀態的函數為pthread_attr_setscope,它有兩個參數,
第一個是指向屬性結構的指標,
第二個是綁定類型,它有兩個取值:
PTHREAD_SCOPE_SYSTEM(綁定的)和
PTHREAD_SCOPE_PROCESS(非綁定的)。
下麵的代碼即創建了一個綁定的執行緒。
#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;
/*初始化屬性值,均設為預設值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid, &attr, (void *) my_function, NULL);
執行緒的分離狀態決定一個執行緒以什麼樣的方式來終止自己。在上面的例子中,我們採用了執行緒的預設屬性,即為非分離狀態,這種情況下,原有的執行緒等待創建的執行緒結束。只有當pthread_join()函數返回時,創建的執行緒才算終止,才能釋放自己佔用的系統資源。
而分離執行緒不是這樣子的,它沒有被其他的執行緒所等待,自己運行結束了,執行緒也就終止了,馬上釋放系統資源。
程式師應該根據自己的需要,選擇適當的分離狀態。
設置執行緒分離狀態的函數為pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。
setdetachstate = set detach state
第二個參數可選為
PTHREAD_CREATE_DETACHED(分離執行緒)和
PTHREAD _CREATE_JOINABLE(非分離執行緒)。
Detached(adj)分離的, 不連接的
這裡要注意的一點是,如果設置一個執行緒為分離執行緒,而這個執行緒運行又非常快,它很可能在pthread_create函數返回之前就終止了,它終止以後就可能將執行緒號和系統資源移交給其他的執行緒使用,這樣調用pthread_create的執行緒就得到了錯誤的執行緒號。
要避免這種情況可以採取一定的同步措施,最簡單的方法之一是可以在被創建的執行緒裡調用pthread_cond_timewait函數,讓這個執行緒等待一會兒,留出足夠的時間讓函數pthread_create返回。
設置一段等待時間,是在多執行緒程式設計裡常用的方法。但是注意不要使用諸如wait()之類的函數,它們是使整個行程睡眠,並不能解決執行緒同步的問題。
另外一個可能常用的屬性是執行緒的優先順序,它存放在結構sched_param中。
用函數pthread_attr_getschedparam和函數pthread_attr_setschedparam進行存放,一般說來,我們總是先取優先順序,對取得的值修改後再存放回去。下面即是一段簡單的例子。
#include <pthread.h>
#include <sched.h>
pthread_attr_t attr;
pthread_t tid;
sched_param param;
int newprio=20;
pthread_attr_init(&attr);
pthread_attr_getschedparam(&attr, ¶m);
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr, ¶m);
pthread_create(&tid, &attr, (void *)myfunction, myarg);
1.4 執行緒的資料處理
和行程(Process)相比,執行緒的最大優點之一是資料的共用性,各個行程(Process)共用父行程(Process)處沿襲的資料段,可以方便的獲得、修改資料。但這也給多執行緒程式設計帶來了許多問題。
我 們必須當心有多個不同的行程(Process)訪問相同的變數。許多函數是不可重入的,即同時不能運行一個函數的多個拷貝(除非使用不同的資料段)。
在函數中聲明的靜態變數 常常帶來問題,函數的返回值也會有問題。因為如果返回的是函數內部靜態聲明的空間的位址,則在一個執行緒調用該函數得到位址後使用該位址指向的資料時,別的 執行緒可能調用此函數並修改了這一段資料。在行程(Process)中共用的變數必須用關鍵字volatile來定義,這是為了防止編譯器在優化時(如gcc中使用-OX參 數)改變它們的使用方式。為了保護變數,我們必須使用信號量、互斥等方法來保證我們對變數的正確使用。
1.4.1 1) 執行緒數據
在單執行緒的程式 裡,有兩種基本的資料:全域變數和區域變數。但在多執行緒程式裡,還有第三種資料類型:執行緒數據(TSD: Thread-Specific Data)。它和全域變數很象,在執行緒內部,各個函數可以象使用全域變數一樣調用它,但它對執行緒外部的其它執行緒是不可見的。這種資料的必要性是顯而易見 的。例如我們常見的變數errno,它返回標準的出錯資訊。它顯然不能是一個區域變數,幾乎每個函數都應該可以調用它;但它又不能是一個全域變數,否則在 A執行緒裡輸出的很可能是B執行緒的出錯資訊。要實現諸如此類的變數,我們就必須使用執行緒資料。我們為每個執行緒資料創建一個鍵,它和這個鍵相關聯,在各個執行緒 裡,都使用這個鍵來指代執行緒資料,但在不同的執行緒裡,這個鍵代表的資料是不同的,在同一個執行緒裡,它代表同樣的資料內容。
和執行緒資料相關的函數主要有4個:創建一個鍵;為一個鍵指定執行緒資料;從一個鍵讀取執行緒資料;刪除鍵。
創建鍵的函數原型為:
extern int pthread_key_create __P ((pthread_key_t *__key,void (*__destr_function) (void *)));
第一個參數為指向一個鍵值的指標,第二個參數指明了一個destructor函數,如果這個參數不為空,那麼當每個執行緒結束時,系統將調用這個函數來釋 放綁定在這個鍵上的區塊。這個函數常和函數pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,為了讓這個鍵只被創建一次。函數pthread_once聲明一個初始化函數,第一次調用pthread_once時它執行這 個函數,以後的調用將被它忽略。
在下面的例子中,我們創建一個鍵,並將它和某個資料相關聯。我們要定義一個函數 createWindow,這個函式定義一個圖形視窗(資料類型為Fl_Window *,這是圖形介面開發工具FLTK中的資料類型)。由於各個執行緒都會調用這個函數,所以我們使用執行緒資料。
/* 聲明一個鍵*/
pthread_key_t myWinKey;
/* 函數 createWindow */
void createWindow ( void ) {
Fl_Window * win;
static pthread_once_t once= PTHREAD_ONCE_INIT;
/* 調用函數createMyKey,創建鍵*/
pthread_once ( & once, createMyKey) ;
/*win指向一個新建立的視窗*/
win=new Fl_Window( 0, 0, 100, 100, "MyWindow");
/* 對此視窗作一些可能的設置工作,如大小、位置、名稱等*/
setWindow(win);
/* 將窗口指標值綁定在鍵myWinKey上*/
pthread_setpecific ( myWinKey, win);
}
/* 函數 createMyKey,創建一個鍵,並指定了destructor */
void createMyKey ( void ) {
pthread_keycreate(&myWinKey, freeWinKey);
}
/* 函數 freeWinKey,釋放空間*/
void freeWinKey ( Fl_Window * win){
delete win;
}
這樣,在不同的執行緒中調用函數createMyWin,都可以得到在執行緒內部均可見的視窗變數,這個變數通過函數 pthread_getspecific得到。在上面的例子中,我們已經使用了函數pthread_setspecific來將執行緒資料和一個鍵綁定在一 起。這兩個函數的原型如下:
extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));
extern void *pthread_getspecific __P ((pthread_key_t __key));
這兩個函數的參數意義和使用方法是顯而易見的。要注意的是,用pthread_setspecific為一個鍵指定新的執行緒資料時,必須自己釋放原有的 執行緒資料以回收空間。這個過程函數pthread_key_delete用來刪除一個鍵,這個鍵佔用的記憶體將被釋放,但同樣要注意的是,它只釋放鍵佔用的 記憶體,並不釋放該鍵關聯的執行緒資料所佔用的記憶體資源,而且它也不會觸發函數pthread_key_create中定義的destructor函數。執行緒 資料的釋放必須在釋放鍵之前完成。
1.4.2 2) 互斥鎖(Mutex)
互斥鎖用來保證一段時間內只有一個執行緒在執行一段代碼。
我們先看下面一段代碼。這是一個讀/寫程式,它們公用一個緩衝區,並且我們假定一個緩衝區只能保存一條資訊。即緩衝區只有兩個狀態:有資訊或沒有資訊。
void reader_function ( void );
void writer_function ( void );
char buffer;
int buffer_has_item=0;
pthread_mutex_t mutex;
struct timespec delay;
void main ( void ){
pthread_t reader;
/* 定義延遲時間*/
delay.tv_sec = 2;
delay.tv_nec = 0;
/* 用預設屬性初始化一個互斥鎖物件*/
pthread_mutex_init (&mutex,NULL);
pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL);
writer_function( );
}
void writer_function (void){
while(1){
/* 鎖定互斥鎖*/
pthread_mutex_lock (&mutex);
if (buffer_has_item==0){
buffer=make_new_item( );
buffer_has_item=1;
}
/* 打開互斥鎖*/
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}
void reader_function(void){
while(1){
pthread_mutex_lock(&mutex);
if(buffer_has_item==1){
consume_item(buffer);
buffer_has_item=0;
}
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}
這裡聲明瞭互斥鎖變數mutex,結構pthread_mutex_t為不公開的資料類型,其中包含一個系統分配的屬性物件。函數 pthread_mutex_init用來生成一個互斥鎖。NULL參數表明使用預設屬性。如果需要聲明特定屬性的互斥鎖,須調用函數 pthread_mutexattr_init。函數pthread_mutexattr_setpshared和函數 pthread_mutexattr_settype用來設置互斥鎖屬性。
函數pthread_mutexattr_setpshared設置屬性pshared,它有兩個取值:
PTHREAD_PROCESS_PRIVATE //用來不同進程中的執行緒同步
PTHREAD_PROCESS_SHARED //用於同步本進程的不同執行緒
在上面的例子中,我們使用的是預設屬性PTHREAD_PROCESS_ PRIVATE。
函數pthread_mutexattr_settype用來設置互斥鎖類型,可選的類型有:
PTHREAD_MUTEX_NORMAL
PTHREAD_MUTEX_ERRORCHECK
PTHREAD_MUTEX_RECURSIVE
PTHREAD _MUTEX_DEFAULT
它們分別定義了不同的上鎖、解鎖機制,一般情況下,選用最後一個預設屬性。
pthread_mutex_lock聲明開始用互斥鎖上鎖,此後的代碼直至調用pthread_mutex_unlock為止,均被上鎖,即同一時間只 能被一個執行緒調用執行。當一個執行緒執行到pthread_mutex_lock處時,如果該鎖此時被另一個執行緒使用,那此執行緒被阻塞,即程式將等待到另一 個執行緒釋放此互斥鎖。在上面的例子中,我們使用了pthread_delay_np函數,讓執行緒睡眠一段時間,就是為了防止一個執行緒始終佔據此函數。
在使用互斥鎖的過程中很有可能會出現鎖死:兩個執行緒試圖同時佔用兩個資源,並按不同的次序鎖定相應的互斥鎖,例如兩個執行緒都需要鎖定互斥鎖1和互斥鎖 2,a執行緒先鎖定互斥鎖1,b執行緒先鎖定互斥鎖2,這時就出現了鎖死。此時我們可以使用函數 pthread_mutex_trylock,它是函數pthread_mutex_lock的非阻塞版本,當它發現鎖死不可避免時,它會返回相應的信 息,程式師可以針對鎖死做出相應的處理。另外不同的互斥鎖類型對鎖死的處理不一樣,但最主要的還是要程式師自己在程式設計注意這一點。
1.4.3 3) 條件變數(Condition)
前一節中我們講述了如何使用互斥鎖來實現執行緒間資料的共用和通信,互斥鎖一個明顯的缺點是它只有兩種狀態:鎖定和非鎖定。而條件變數通過允許執行緒阻塞和 等待另一個執行緒發送信號的方法彌補了互斥鎖的不足,它常和互斥鎖一起使用。使用時,條件變數被用來阻塞一個執行緒,當條件不滿足時,執行緒往往解開相應的互斥 鎖並等待條件發生變化。一旦其它的某個執行緒改變了條件變數,它將通知相應的條件變數喚醒一個或多個正被此條件變數阻塞的執行緒。這些執行緒將重新鎖定互斥鎖並 重新測試條件是否滿足。一般說來,條件變數被用來進行線承間的同步。
條件變數的結構為pthread_cond_t,函數pthread_cond_init()被用來初始化一個條件變數。它的原型為:
extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));
其中cond是一個指向結構pthread_cond_t的指標,cond_attr是一個指向結構pthread_condattr_t的指標。結構 pthread_condattr_t是條件變數的屬性結構,和互斥鎖一樣我們可以用它來設置條件變數是進程內可用還是進程間可用,預設值是 PTHREAD_ PROCESS_PRIVATE,即此條件變數被同一進程內的各個執行緒使用。注意初始化條件變數只有未被使用時才能重新初始化或被釋放。釋放一個條件變數 的函數為pthread_cond_ destroy(pthread_cond_t cond)。
函數pthread_cond_wait()使執行緒阻塞在一個條件變數上。它的函數原型為:
extern int pthread_cond_wait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex));
執行緒解開mutex指向的鎖並被條件變數cond阻塞。執行緒可以被函數pthread_cond_signal和函數 pthread_cond_broadcast喚醒,但是要注意的是,條件變數只是起阻塞和喚醒執行緒的作用,具體的判斷條件還需用戶給出,例如一個變數是 否為0等等,這一點我們從後面的例子中可以看到。執行緒被喚醒後,它將重新檢查判斷條件是否滿足,如果還不滿足,一般說來執行緒應該仍阻塞在這裡,被等待被下 一次喚醒。這個過程一般用while語句實現。
另一個用來阻塞執行緒的函數是pthread_cond_timedwait(),它的原型為:
extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex, __const struct timespec *__abstime));
它比函數pthread_cond_wait()多了一個時間參數,經歷abstime段時間後,即使條件變數不滿足,阻塞也被解除。
函數pthread_cond_signal()的原型為:
extern int pthread_cond_signal __P ((pthread_cond_t *__cond));
它用來釋放被阻塞在條件變數cond上的一個執行緒。多個執行緒阻塞在此條件變數上時,哪一個執行緒被喚醒是由執行緒的調度策略所決定的。要注意的是,必須用保 護條件變數的互斥鎖來保護這個函數,否則條件滿足信號又可能在測試條件和調用pthread_cond_wait函數之間被發出,從而造成無限制的等待。 下面是使用函數pthread_cond_wait()和函數pthread_cond_signal()的一個簡單的例子。
pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count;
decrement_count () {
pthread_mutex_lock (&count_lock);
while(count==0)
pthread_cond_wait( &count_nonzero, &count_lock);
count=count -1;
pthread_mutex_unlock (&count_lock);
}
increment_count(){
pthread_mutex_lock(&count_lock);
if(count==0)
pthread_cond_signal(&count_nonzero);
count=count+1;
pthread_mutex_unlock(&count_lock);
}
count值為0時, decrement函數在pthread_cond_wait處被阻塞,並打開互斥鎖count_lock。此時,當調用到函數 increment_count時,pthread_cond_signal()函數改變條件變數,告知decrement_count()停止阻塞。讀 者可以試著讓兩個執行緒分別運行這兩個函數,看看會出現什麼樣的結果。
函數pthread_cond_broadcast(pthread_cond_t *cond)用來喚醒所有被阻塞在條件變數cond上的執行緒。這些執行緒被喚醒後將再次競爭相應的互斥鎖,所以必須小心使用這個函數。
1.4.4 4) 信號量(Semaphore)
信號量本質上是一個非負的整數計數器,它被用來控制對公共資源的訪問。當公共資源增加時,調用函數sem_post()增加信號量。只有當信號量值大於 0時,才能使用公共資源,使用後,函數sem_wait()減少信號量。函數sem_trywait()和函數pthread_ mutex_trylock()起同樣的作用,它是函數sem_wait()的非阻塞版本。下面我們逐個介紹和信號量有關的一些函數,它們都在標頭檔 /usr/include/semaphore.h中定義。
信號量的資料類型為結構sem_t,它本質上是一個長整型的數。函數sem_init()用來初始化一個信號量。它的原型為:
extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
sem為指向信號量結構的一個指標;pshared不為0時此信號量在進程間共用,否則只能為當前進程的所有執行緒共用;value給出了信號量的初始值。
函數sem_post( sem_t *sem )用來增加信號量的值。當有執行緒阻塞在這個信號量上時,調用這個函數會使其中的一個執行緒不在阻塞,選擇機制同樣是由執行緒的調度策略決定的。
函數sem_wait( sem_t *sem )被用來阻塞當前執行緒直到信號量sem的值大於0,解除阻塞後將sem的值減一,表明公共資源經使用後減少。函數sem_trywait ( sem_t *sem )是函數sem_wait()的非阻塞版本,它直接將信號量sem的值減一。
函數sem_destroy(sem_t *sem)用來釋放信號量sem。
下面我們來看一個使用信號量的例子。在這個例子中,一共有4個執行緒,其中兩個執行緒負責從檔讀取資料到公共的緩衝區,另兩個執行緒從緩衝區讀取資料作不同的處理(加和乘運算)。
/* File sem.c */
#include
#include
#include
#define MAXSTACK 100
int stack[MAXSTACK][2];
int size=0;
sem_t sem;
/* 從文件1.dat讀取資料,每讀一次,信號量加一*/
void ReadData1(void){
FILE *fp=fopen("1.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/*從文件2.dat讀取數據*/
void ReadData2(void){
FILE *fp=fopen("2.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/*阻塞等待緩衝區有資料,讀取資料後,釋放空間,繼續等待*/
void HandleData1(void){
while(1){
sem_wait(&sem);
printf("Plus:%d+%d=%d\n",stack[size][0],stack[size][1],
stack[size][0]+stack[size][1]);
--size;
}
}
void HandleData2(void){
while(1){
sem_wait(&sem);
printf("Multiply:%d*%d=%d\n",stack[size][0],stack[size][1],
stack[size][0]*stack[size][1]);
--size;
}
}
int main(void){
pthread_t t1,t2,t3,t4;
sem_init(&sem,0,0);
pthread_create(&t1,NULL,(void *)HandleData1,NULL);
pthread_create(&t2,NULL,(void *)HandleData2,NULL);
pthread_create(&t3,NULL,(void *)ReadData1,NULL);
pthread_create(&t4,NULL,(void *)ReadData2,NULL);
/* 防止程式過早退出,讓它在此無限期等待*/
pthread_join(t1,NULL);
}
在Linux下,我們用命令gcc -lpthread sem.c -o sem生成可執行檔sem。 我們事先編輯好資料檔案1.dat和2.dat,假設它們的內容分別為1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,我們運行sem,得到如下的結果:
Multiply:-1*-2=2
Plus:-1+-2=-3
Multiply:9*10=90
Plus:-9+-10=-19
Multiply:-7*-8=56
Plus:-5+-6=-11
Multiply:-3*-4=12
Plus:9+10=19
Plus:7+8=15
Plus:5+6=11
從中我們可以看出各個執行緒間的競爭關係。而數值並未按我們原先的順序顯示出來這是由size這個數值被各個執行緒任意修改的緣故。這也往往是多執行緒程式設計要注意的問題。
1.4.5 5)非同步信號
由 於LinuxThreads是在核外使用核內羽量級進程實現的執行緒,所以基於內核的非同步信號操作對於執行緒也是有效的。但同時,由於非同步信號總是實際發往某 個進程,所以無法實現POSIX標準所要求的"信號到達某個進程,然後再由該進程將信號分發到所有沒有阻塞該信號的執行緒中"原語,而是只能影響到其中一個 執行緒。
POSIX非同步信號同時也是一個標準C庫提供的功能,主要包括信號集管理(sigemptyset()、sigfillset()、 sigaddset()、sigdelset()、sigismember()等)、信號處理函數安裝(sigaction())、信號阻塞控制 (sigprocmask())、被阻塞信號查詢(sigpending())、信號等待(sigsuspend())等,它們與發送信號的kill() 等函數配合就能實現進程間非同步信號功能。LinuxThreads圍繞執行緒封裝了sigaction()何raise(),本節集中討論 LinuxThreads中擴展的非同步信號函數,包括pthread_sigmask()、pthread_kill()和sigwait()三個函數。 毫無疑問,所有POSIX非同步信號函數對於執行緒都是可用的。
int pthread_sigmask(int how, const sigset_t *newmask, sigset_t *oldmask)
設置執行緒的信號遮罩碼,語義與sigprocmask()相同,但對不允許遮罩的Cancel信號和不允許回應的Restart信號進行了保護。被遮罩的信號保存在信號佇列中,可由sigpending()函數取出。
int pthread_kill(pthread_t thread, int signo)
向thread號執行緒發送signo信號。實現中在通過thread執行緒號定位到對應進程號以後使用kill()系統調用完成發送。
int sigwait(const sigset_t *set, int *sig)
掛 起執行緒,等待set中指定的信號之一到達,並將到達的信號存入*sig中。POSIX標準建議在調用sigwait()等待信號以前,進程中所有執行緒都應 遮罩該信號,以保證僅有sigwait()的調用者獲得該信號,因此,對於需要等待同步的非同步信號,總是應該在創建任何執行緒以前調用 pthread_sigmask()遮罩該信號的處理。而且,調用sigwait()期間,原來附接在該信號上的信號處理函數不會被調用。
如果在等待期間接收到Cancel信號,則立即退出等待,也就是說sigwait()被實現為取消點。
1.4.6 6)關於執行緒的撤銷
在多執行緒中,某些情況下可能需要撤銷一個執行緒,恢復執行緒修改了的一些變數,並且釋放執行緒佔用的一些共用資源,將系統返回到這個執行緒執行的初始狀態或特定狀 態。在處理執行緒撤銷時需要小心處理。不能遺留已上鎖的交互鎖(這會導致鎖死),也不能留下末釋放的記憶體區(這將導致記憶體洩漏)。
pthread提供了撤銷執行緒的相關介面函數,這些介面可用於允許或者禁止撤銷一個執行緒,設定撤銷點等。
調用帶有一個執行緒ID作為參數的函數pthread_cancel()可以撤銷一個執行緒。函數pthread_setcancelstate()和 pthread_setcanceltype()用來設定目標執行緒的狀態和屬性值,這些值確定執行緒如何回應撤銷請求。
pthread_setcancelstate()可以將執行緒設置為PTHREAD-CANCEL-ENABLE或者PTHREAD-CANCEL- DISABLE兩種狀態。如果執行緒的狀態是PTHREAD-CANCEL-DISABLE,那麼所有對那個執行緒的撤銷請求將被掛起。如果狀態為 PTHREAD-CANCEL-ENABLE,那麼撤銷行為依賴於這個執行緒的撤銷類型。執行緒的撤銷類型由函數pthread_setcanceltype ()設定。
pthread_setcanceltype()可以將執行緒的撤銷類型設置為PTHREAD-CANCEL-DEFERRED或者PTHREAD- CANCEL-ASYNCHRONOUS。如果撤銷類型是PTHREAD-CANCEL-ASYNCHRONOUS,接受到一個 pthread_cancel()調用後,執行緒會立即撤銷。如果撤銷類型是PTHREAD_CANCEL-DEFERRED,那麼直到執行緒達到一個撤銷點 時才發生撤銷。
通過在執行緒代碼中插入函式呼叫pthread_testcancel()就可以建立一個撤銷點。當此函數執行時,如果掛起一個撤銷,那麼pthread_testcancel()將不返回。
除了使用pthread_testcancel()外,pthreads還定義一些撤銷點。包括等待在pthread_cond_timedwait() 和pthread_cond_wait()上的執行緒、等待在pthread_join()中的另一執行緒結束的執行緒、阻塞在sigwait()上的執行緒。也 有許多可作為撤銷點的標準庫調用。通常這些函數會使執行緒在它上面阻塞。
pthread庫提供了一對pthread_cleanup_push()/pthread_cleanup_pop()函數對用於自動釋放資源--從 pthread_cleanup_push()的調用點到pthread_cleanup_pop()之間的程式段中的終止動作(包括調用 pthread_exit()和取消點終止)都將執行pthread_cleanup_push()所指定的清理函數。兩個函式定義如下:
void pthread_cleanup_push(void (*routine) (void *), void *arg);
void pthread_cleanup_pop(int execute);
這 兩個函數採用先入後出的棧結構管理,void routine(void *arg)函數在調用pthread_cleanup_push()時壓入清理函數棧,多次對pthread_cleanup_push()的調用將在清 理函數棧中形成一個函數鏈,在執行該函數鏈時按照壓棧的相反順序彈出。execute參數表示執行到pthread_cleanup_pop()時是否在 彈出清理函數的同時執行該函數,為0表示不執行,非0為執行;這個參數並不影響異常終止時清理函數的執行。
pthread_cleanup_push()/pthread_cleanup_pop()是以巨集方式實現的,這是pthread.h中的巨集定義:
#define pthread_cleanup_push(routine,arg) \
{ struct _pthread_cleanup_buffer _buffer; _pthread_cleanup_push (&_buffer, (routine), (arg));
#define pthread_cleanup_pop(execute) _pthread_cleanup_pop (&_buffer, (execute)); }
可 見,pthread_cleanup_push()帶有一個"",因此這兩個函數必須成對出現,且必須位於程式的同一級別的程式碼片段中才能通過編譯。在下面 的例子裡,當執行緒在"do some work"中終止時,將主動調用pthread_mutex_unlock(mut),以完成解鎖動作。
pthread_cleanup_push(pthread_mutex_unlock, (void *) &mut);
pthread_mutex_lock(&mut);
/* do some work */
pthread_mutex_unlock(&mut);
pthread_cleanup_pop(0);
必 須要注意的是,如果執行緒處於PTHREAD_CANCEL_ASYNCHRONOUS狀態,上述程式碼片段就有可能出錯,因為CANCEL事件有可能在 pthread_cleanup_push()和pthread_mutex_lock()之間發生,或者在pthread_mutex_unlock ()和pthread_cleanup_pop()之間發生,從而導致清理函數unlock一個並沒有加鎖的mutex變數,造成錯誤。因此,在使用清理 函數的時候,都應該暫時設置成PTHREAD_CANCEL_DEFERRED模式。為此,POSIX的Linux實現中還提供了一對不保證可移植的擴展 函數:
pthread_cleanup_push_defer_np()
pthread_cleanup_pop_defer_np()
功能與以下程式碼片段相當:
{
int oldtype;
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &oldtype);
pthread_cleanup_push(routine, arg);
...
pthread_cleanup_pop(execute);
pthread_setcanceltype(oldtype, NULL);
}
1.5 小結
多執行緒程式設計是一個很有意思也很有用的技術,使用多執行緒技術的網路螞蟻是目前最常用的下載工具之一,使用多執行緒技術的grep比單執行緒的grep要快上幾倍,類似的例子還有很多。希望大家能用多執行緒技術寫出高效實用的好程式來。
2 同步和非同步與阻塞和非阻塞的區別
http://blog.huanghao.me/?p=95
發現我好像越來越喜歡糾纏於這些定義=_=。
同步和非同步與阻塞與非阻塞是在通信和I/O中常用的字眼,之前在許多地方同步與阻塞,非同步與非阻塞常常被混為一談,帶來了許多混亂,其實同步、非同步和阻塞、非阻塞是兩個不同的概念。最近隨著非同步IO(AIO)越來越多的應用,對這兩個概念進行區分和解釋的文章也越來越多,但是問起身邊的同學,能說清楚的倒也不多,所以我就順便跟風寫一篇科普文吧(越來越水了=_=)。
2.1 同步(synchronous)和非同步(asynchronous)
同步(synchronous)和非同步(asynchronous)其實是針對消息的發送和接受的次序而言的(在通信中就是消息的發送和接收,在IO中就是資料的讀和寫)。同步的意思就是消息的發送和接收是有序的,即接收和發送第二個包一定在第一個包之後第三個包之前,而不是亂序。非同步的意思就是消息的發送和接收是可以亂序的,第一個包沒發完可以直接發第二個包。
2.2 阻塞(block)和非阻塞(non-block)
至於阻塞(block)和非阻塞(non-block)其實描述的是進程或執行緒進行等待時的一種方式。阻塞的意思是等待時進程或執行緒需要掛起,而非阻塞則是等待時執行緒或進程不需要被掛起,不影響執行緒的執行,這時執行緒或進程可以繼續處理其它事物,不因為這個等待而受到影響(當然它仍然在等待這個消息,只不過可能會在執行緒或進程執行週期的某一個地方去查看消息的通知,而不是立即在原地等待)。
舉個例子,兩個人之間發短信,最簡單的就是同步阻塞的方式,一個人發短信,然後啥也不幹地等在手機前面,直到對方回信,接下來才發第二條短信(這時也確認了第一條短信已發到)。而同步非阻塞方式也就是大家常用的方式,則是發出去消息,然後去幹別的事,(體現了非阻塞)等對方回短信之後(相當於確認了第一條短信已收到,並且有後續資料過來),再發第二條短信(體現了同步)。非同步阻塞的方式,則是一口氣發出幾十條短信(由於中國移動並不保證發出短信的先後順序,可能導致對方收到短信的順序和發出去時不一致,這就體現了非同步的概念,而且理論上發信的順序也可以是亂的),發完之後就啥也不幹,等對方一條一條的回信(這體現了阻塞的概念)。而如果在一口氣發出幾十條短信後沒有傻傻的等待,而是去別的地方玩去了,對方的回信到一條讀一條,則就變成非同步非阻塞的方式了。
不知道通過上面的例子,大家是不是已經可以理解這兩組概念之間的區別了。這裡有篇相關的文章寫得不錯,如果還有些不理解的,可以再去閱讀一下。由於國內在IT領域的起步落後國外(主要是美國)一些年份,再加上互聯網的迅速普及,導致許多以訛傳訛的現象時有發生。這兩組本來適用範圍並不相同的概念卻在很長一段時間內被混為一談,應該就是這方面的例子。這種錯誤增加了大家的學習成本,也不利於在某一些領域的進一步研究,所以個人以為搞清楚這些概念還是很有必要的(最後為自己的又一篇水文開脫一下=_=)
3 pthread_join和pthread_detach的用法
http://huoyj.iteye.com/blog/1907529
阻塞(block)和非阻塞(non-block)其實描述的是進程或線程進行等待時的一種方式。
阻塞的意思是等待時進程或線程需要掛起。
掛起,
是指在作業系統行程管理將前臺的行程暫停並轉入後台的動作。將行程掛起可以讓使用者在前臺執行其他的行程。掛起的行程通常釋放除CPU以外已經佔有的系統資源,如記憶體等)。
3.1 一:關於join
join
join是三種同步執行緒的方式之一。另外兩種分別是互斥鎖(mutex)和條件變數(condition variable)。
調用pthread_join()將阻塞自己,一直到要等待加入的執行緒運行結束。
可以用pthread_join()獲取執行緒的返回值。
一個執行緒對應一個pthread_join()調用,對同一個執行緒進行多次pthread_join()調用是邏輯錯誤。
join or detach
執行緒分兩種:一種可以join,另一種不可以。該屬性在創建執行緒的時候指定。
joinable執行緒可在創建後,用pthread_detach()顯式地分離。但分離後不可以再合併。該操作不可逆。
為了確保移植性,在創建執行緒時,最好顯式指定其join或detach屬性。似乎不是所有POSIX實現都是用joinable作默認。
3.2 二: pthread_detach
創建一個執行緒預設的狀態是joinable, 如果一個執行緒結束運行但沒有被join,則它的狀態類似於進程中的Zombie Process,即還有一部分資源沒有被回收(退出狀態碼),所以創建執行緒者應該調用pthread_join來等待中的執行緒運行結束,並可得到執行緒的退出代碼,回收其資源(類似於wait,waitpid)
但是調用pthread_join(pthread_id)後,如果該執行緒沒有運行結束,調用者會被阻塞,在有些情況下我們並不希望如此,比如在Web伺服器中當主執行緒為每個新來的連結創建一個子執行緒進行處理的時候,主執行緒並不希望因為調用pthread_join而阻塞(因為還要繼續處理之後到來的連結),這時可以在子執行緒中加入代碼
pthread_detach(pthread_self())
或者父執行緒調用
pthread_detach(thread_id)(非阻塞,可立即返回)
這將該子執行緒的狀態設置為detached,則該執行緒運行結束後會自動釋放所有資源。
3.3 三:pthread_join
調用pthread_join的執行緒會阻塞,直到指定的執行緒返回,調用了pthread_exit,或者被取消。
如果執行緒簡單的返回,那麼rval_ptr被設置成執行緒的返回值,參見範例1;如果調用了pthread_exit,則可將一個無類型指標返回,在pthread_join中對其進行訪問,參見範例2;如果執行緒被取消,rval_ptr被設置成PTHREAD_CANCELED。
如果我們不關心執行緒的返回值,那麼我們可以把rval_ptr設置為NULL。
範例1:
#include <pthread.h>
#include <string.h>
void *thr_fn1(void *arg)
{
printf(“thread 1 returning.\n”);
return((void *)1);
}
void *thr_fn2(void *arg)
{
printf(“thread 2 exiting.\n”);
return((void *)2);
}
int main()
{
pthread_t tid1,tid2;
void *tret;
pthread_create(&tid1,NULL,thr_fn1,NULL);
pthread_create(&tid2,NULL,thr_fn2,NULL);
pthread_join(tid1,&tret);
printf(“thread 1 exit code %d\n”,(int)tret);
pthread_join(tid2,&tret);
printf(“thread 2 exit code %d\n”,(int)tret);
exit(0);
}
運行結果:
thread 1 returning.
thread 1 exit code 1.
thread 2 exiting.
thread 2 exit code 2.
範例2:
#include <stdio.h>
#include <pthread.h>
void thread1(char s[])
{
printf("This is a pthread1.\n");
printf("%s\n",s);
pthread_exit("Hello first!"); //結束執行緒,返回一個值。
}
void thread2(char s[])
{
printf("This is a pthread2.\n");
printf("%s\n",s);
pthread_exit("Hello second!");
}
int main(void)
{
pthread_t id1,id2;
void *a1,*a2;
int i,ret1,ret2;
char s1[]="This is first thread!";
char s2[]="This is second thread!";
ret1=pthread_create(&id1,NULL,(void *) thread1,s1);
ret2=pthread_create(&id2,NULL,(void *) thread2,s2);
if(ret1!=0){
printf ("Create pthread1 error!\n");
exit (1);
}
pthread_join(id1,&a1);
printf("%s\n",(char*)a1);
if(ret2!=0){
printf ("Create pthread2 error!\n");
exit (1);
}
printf("This is the main process.\n");
pthread_join(id2,&a2);
printf("%s\n",(char*)a2);
return (0);
}
運行結果:
[****@XD**** c]$ ./example
This is a pthread1.
This is first thread!
Hello first!
This is the main process.
This is a pthread2.
3.4 <參考資料語>
一般情況下,進程中各個執行緒的運行都是相互獨立的,執行緒的終止並不會通知,也不會影響其他執行緒,終止的執行緒所佔用的資源也並不會隨著執行緒的終止而得到釋 放。正如進程之間可以用wait()系統調用來同步終止並釋放資源一樣,執行緒之間也有類似機制,那就是pthread_join()函數
pthread_join()的調用者將掛起並等待th執行緒終止,retval是pthread_exit()調用者執行緒(執行緒ID為th)的返回值,如 果thread_return不為NULL,則*thread_return=retval。需要注意的是一個執行緒僅允許唯一的一個執行緒使用 pthread_join()等待它的終止,並且被等待的執行緒應該處於可join狀態,即非DETACHED狀態
如果進程中的某個執行緒執行了pthread_detach(th),則th執行緒將處於DETACHED狀態,這使得th執行緒在結束運行時自行釋放所佔用的 記憶體資源,同時也無法由pthread_join()同步,pthread_detach()執行之後,對th請求pthread_join()將返回錯誤
一個可join的執行緒所佔用的記憶體僅當有執行緒對其執行了pthread_join()後才會釋放,因此為了避免記憶體洩漏,所有執行緒的終止,要麼已設為DETACHED,要麼就需要使用pthread_join()來回收
3) 主執行緒用pthread_exit還是return
用pthread_exit只會使主執行緒自身退出,產生的子執行緒繼續執行;用return則所有執行緒退出。
綜合以上要想讓子執行緒總能完整執行(不會中途退出),一種方法是在主執行緒中調用pthread_join對其等待,即pthread_create/pthread_join/pthread_exit或return;一種方法是在主執行緒退出時使用pthread_exit,這樣子執行緒能繼續執行,即pthread_create/pthread_detach/pthread_exit;還有一種是pthread_create/pthread_detach/return,這時就要保證主執行緒不能退出,至少是子執行緒完成前不能退出。現在的項目中用的就是第三種方法,主執行緒是一個閉環,子執行緒有的是閉環有的不是。
3.5 <參考資料語>
理論上說,pthread_exit()和執行緒宿體函數退出的功能是相同的,函數結束時會在內部自動調用pthread_exit()來清理執行緒相關的資源。但實際上二者由於編譯器的處理有很大的不同。
在進程主函數(main())中調用pthread_exit(),只會使主函數所在的執行緒(可以說是進程的主執行緒)退出;而如果是return,編譯器將使其調用進程退出的代碼(如_exit()),從而導致進程及其所有執行緒結束運行。
目錄
1 引言
2 簡單的多執行緒程式設計
3 修改執行緒的屬性
4 執行緒的資料處理
4.1 1) 執行緒數據
4.2 2) 互斥鎖
4.3 3) 條件變數
4.4 4) 信號量
4.5 5)非同步信號
4.6 6)關於執行緒的撤銷
1.1 引言
執行緒(thread)技術早在60年代就被提出,但真正應用多執行緒到作業系統中去,是在80年代中期,solaris是這方面的佼佼者。傳統的Unix也支持執行緒的概念,但是在一個行程(process)中只允許有一個執行緒,這樣多執行緒就意味著多行程(process)。現在,多執行緒技術已經被許多作業系統所支援,包括Windows/NT,當然,也包括Linux。
為什麼有了行程(process)的概念後,還要再引入執行緒呢?使用多執行緒到底有哪些好處?什麼的系統應該選用多執行緒?我們首先必須回答這些問題。
使用多執行緒的理由之一是和行程(process)相比,它是一種非常"節儉"的多工操作方式。我們知道,在Linux系統下,啟動一個新的行程(process)必須分配給它獨立的位址空間,建立眾多的資料表來維護它的程式碼片段、堆疊段和資料段,這是一種"昂貴"的多工工作方式。而運行於一個行程(process)中的多個執行緒,它們彼此之間使用相同的位址空間,共用大部分數據,啟動一個執行緒所花費的空間遠遠小於啟動一個行程(process)所花費的空間,而且,執行緒間彼此切換所需的時間也遠遠小於行程(process)間切換所需要的時間。據統計,總的說來,?0倍左右,當然,在具體的系統上,這個資料可能會有較大的區別。
使用多執行緒的理由之二是執行緒間方便的通信機制。對不同行程(process)來說,它們具有獨立的資料空間,要進行資料的傳遞只能通過通信的方式進行,這種方式不僅費時,而且很不方便。執行緒則不然,由於同一行程(process)下的執行緒之間共用資料空間,所以一個執行緒的資料可以直接為其它執行緒所用,這不僅快捷,而且方便。當然,資料的共用也帶來其他一些問題,有的變數不能同時被兩個執行緒所修改,有的副程式中聲明為static的資料更有可能給多執行緒程式帶來災難性的打擊,這些正是編寫多執行緒程式時最需要注意的地方。
除了以上所說的優點外,不和行程(process)比較,多執行緒程式作為一種多工、併發的工作方式,當然有以下的優點:
1) 提高應用程式回應。
這對圖形介面的程式尤其有意義,當一個操作耗時很長時,整個系統都會等待這個操作,此時程式不會回應鍵盤、滑鼠、功能表的操作,而使用多執行緒技術,將耗時長的操作(time consuming)置於一個新的執行緒,可以避免這種尷尬的情況。
2) 使多CPU系統更加有效。
作業系統會保證當執行緒數不大於CPU數目時,不同的執行緒運行於不同的CPU上。
3) 改善程式結構。
一個既長又複雜的行程(process)可以考慮分為多個執行緒,成為幾個獨立或半獨立的運行部分,這樣的程式會利於理解和修改。
下麵我們先來嘗試編寫一個簡單的多執行緒程式。
1.2 簡單的多執行緒程式設計
Linux系統下的多執行緒遵循POSIX執行緒介面,稱為pthread。
編寫Linux下的多執行緒程式,需要使用標頭檔pthread.h,連接時需要使用庫libpthread.a。
順便說一下,Linux下pthread的實現是通過系統調用clone( )來實現的。
clone( )是Linux所特有的系統調用,它的使用方式類似fork,關於clone( )的詳細情況,有興趣的讀者可以去查看有關文檔說明。
下面我們展示一個最簡單的多執行緒程式example1.c。
/* example.c*/
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h> // for exit() 更正
void thread(void)
{
int i;
for(i=0;i<3;i++)
printf("This is a pthread.\n");
}
int main(void)
{
pthread_t id;
int i,ret;
ret=pthread_create(&id,NULL,(void *) thread,NULL);
if(ret!=0){
printf ("Create pthread error!n");
exit (1);
}
for(i=0;i<3;i++)
printf("This is the main process.\n");
pthread_join(id,NULL);
return (0);
}
我們編譯此程式:
gcc example1.c -lpthread -o example1
運行example1,我們得到如下結果:
This is the main process.
This is a pthread.
This is the main process.
This is the main process.
This is a pthread.
This is a pthread.
再次運行,我們可能得到如下結果:
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
前後兩次結果不一樣,這是兩個執行緒爭奪CPU資源的結果。
上面的示例中,我們使用到了兩個函數,pthread_create和pthread_join,並聲明瞭一個pthread_t型的變數。
pthread_t在標頭檔/usr/include/bits/pthreadtypes.h中定義:
typedef unsigned long int pthread_t;
它是一個執行緒的識別字。函數pthread_create用來創建一個執行緒,它的原型為:
extern int pthread_create __P
(( pthread_t *__thread,
__const pthread_attr_t *__attr,
void *(*__start_routine) (void *),
void *__arg));
第一個參數為指向執行緒識別字的指標,
第二個參數用來設置執行緒屬性,
第三個參數是執行緒運行函數的起始位址,
最後一個參數是運行函數的參數。
這裡,我們的函數thread不需要參數,所以最後一個參數設為空指標(NULL)。第二個參數我們也設為空指標(NULL),這樣將生成預設屬性的執行緒。對執行緒屬性的設定和修改我們將在下一節闡述。
當創建執行緒成功時,函數返回0,若不為0則說明創建執行緒失敗,常見的錯誤返回代碼為EAGAIN和EINVAL。
前者(EAGAIN)表示系統限制創建新的執行緒,例如執行緒數目過多了;
後者(EINVAL)表示第二個參數代表的執行緒屬性值非法。
創建執行緒成功後,新創建的執行緒則運行參數三和參數四確定的函數,原來的執行緒則繼續運行下一行代碼。
函數pthread_join用來等待一個執行緒的結束。
函數原型為:
extern int pthread_join __P ((pthread_t __th, void **__thread_return));
第一個參數為被等待的執行緒識別字,
第二個參數為一個用戶定義的指標,它可以用來存儲被等待中的執行緒的返回值。
這個函數是一個執行緒阻塞的函數,調用它的函數將一直等待到被等待的執行緒結束為止,當函數返回時,被等待中的執行緒的資源被收回。
一個執行緒的結束有兩種途徑,一種是象我們上面的例子一樣,函數結束了,調用它的執行緒也就結束了;
另一種方式是通過函數pthread_exit來實現。它的函數原型為:
extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
唯一的參數是函數的返回代碼,只要pthread_join中的第二個參數thread_return不是NULL,這個值將被傳遞給thread_return。
最後要說明的是,一個執行緒不能被多個執行緒等待,否則第一個接收到信號的執行緒成功返回,其餘調用pthread_join的執行緒則返回錯誤代碼ESRCH。
在這一節裡,我們編寫了一個最簡單的執行緒,並掌握了最常用的三個函數pthread_create,pthread_join和pthread_exit。下面,我們來瞭解執行緒的一些常用屬性以及如何設置這些屬性。
1.3 修改執行緒的屬性
在上一節的例子裡,我們用pthread_create函數創建了一個執行緒,在這個執行緒中,我們使用了預設參數,即將該函數的第二個參數設為NULL。的確,對大多數程式來說,使用預設屬性就夠了,但我們還是有必要來瞭解一下執行緒的有關屬性。
屬性結構為pthread_attr_t,它同樣在標頭檔/usr/include/pthread.h中定義,喜歡追根問底的人可以自己去查看。屬性值不能直接設置,須使用相關函數進行操作,初始化的函數為pthread_attr_init,這個函數必須在pthread_create函數之前調用。
屬性物件主要包括是否綁定、是否分離、堆疊位址、堆疊大小、優先順序。
預設的屬性為非綁定、非分離、缺省1M的堆疊、與父進程同樣級別的優先順序。
關於執行緒的綁定,牽涉到另外一個概念:輕行程(LWP:Light Weight Process)。
輕行程可以理解為內核執行緒,它位於使用者層和系統層之間。系統對執行緒資源的分配、對執行緒的控制是通過輕行程來實現的,一個輕行程可以控制一個或多個執行緒。預設狀況下,啟動多少輕行程、哪些輕行程來控制哪些執行緒是由系統來控制的,這種狀況即稱為非綁定的。
綁定狀況下,則顧名思義,即某個執行緒固定的"綁"在一個輕行程之上。
被綁定的執行緒具有較高的回應速度,這是因為CPU時間片的調度是面向輕行程的,
綁定的執行緒可以保證在需要的時候它總有一個輕行程可用。
通過設置被綁定的輕行程的優先順序和調度級可以使得綁定的執行緒滿足諸如即時反應之類的要求。
設置執行緒綁定狀態的函數為pthread_attr_setscope,它有兩個參數,
第一個是指向屬性結構的指標,
第二個是綁定類型,它有兩個取值:
PTHREAD_SCOPE_SYSTEM(綁定的)和
PTHREAD_SCOPE_PROCESS(非綁定的)。
下麵的代碼即創建了一個綁定的執行緒。
#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;
/*初始化屬性值,均設為預設值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid, &attr, (void *) my_function, NULL);
執行緒的分離狀態決定一個執行緒以什麼樣的方式來終止自己。在上面的例子中,我們採用了執行緒的預設屬性,即為非分離狀態,這種情況下,原有的執行緒等待創建的執行緒結束。只有當pthread_join()函數返回時,創建的執行緒才算終止,才能釋放自己佔用的系統資源。
而分離執行緒不是這樣子的,它沒有被其他的執行緒所等待,自己運行結束了,執行緒也就終止了,馬上釋放系統資源。
程式師應該根據自己的需要,選擇適當的分離狀態。
設置執行緒分離狀態的函數為pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。
setdetachstate = set detach state
第二個參數可選為
PTHREAD_CREATE_DETACHED(分離執行緒)和
PTHREAD _CREATE_JOINABLE(非分離執行緒)。
Detached(adj)分離的, 不連接的
這裡要注意的一點是,如果設置一個執行緒為分離執行緒,而這個執行緒運行又非常快,它很可能在pthread_create函數返回之前就終止了,它終止以後就可能將執行緒號和系統資源移交給其他的執行緒使用,這樣調用pthread_create的執行緒就得到了錯誤的執行緒號。
要避免這種情況可以採取一定的同步措施,最簡單的方法之一是可以在被創建的執行緒裡調用pthread_cond_timewait函數,讓這個執行緒等待一會兒,留出足夠的時間讓函數pthread_create返回。
設置一段等待時間,是在多執行緒程式設計裡常用的方法。但是注意不要使用諸如wait()之類的函數,它們是使整個行程睡眠,並不能解決執行緒同步的問題。
另外一個可能常用的屬性是執行緒的優先順序,它存放在結構sched_param中。
用函數pthread_attr_getschedparam和函數pthread_attr_setschedparam進行存放,一般說來,我們總是先取優先順序,對取得的值修改後再存放回去。下面即是一段簡單的例子。
#include <pthread.h>
#include <sched.h>
pthread_attr_t attr;
pthread_t tid;
sched_param param;
int newprio=20;
pthread_attr_init(&attr);
pthread_attr_getschedparam(&attr, ¶m);
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr, ¶m);
pthread_create(&tid, &attr, (void *)myfunction, myarg);
1.4 執行緒的資料處理
和行程(Process)相比,執行緒的最大優點之一是資料的共用性,各個行程(Process)共用父行程(Process)處沿襲的資料段,可以方便的獲得、修改資料。但這也給多執行緒程式設計帶來了許多問題。
我 們必須當心有多個不同的行程(Process)訪問相同的變數。許多函數是不可重入的,即同時不能運行一個函數的多個拷貝(除非使用不同的資料段)。
在函數中聲明的靜態變數 常常帶來問題,函數的返回值也會有問題。因為如果返回的是函數內部靜態聲明的空間的位址,則在一個執行緒調用該函數得到位址後使用該位址指向的資料時,別的 執行緒可能調用此函數並修改了這一段資料。在行程(Process)中共用的變數必須用關鍵字volatile來定義,這是為了防止編譯器在優化時(如gcc中使用-OX參 數)改變它們的使用方式。為了保護變數,我們必須使用信號量、互斥等方法來保證我們對變數的正確使用。
1.4.1 1) 執行緒數據
在單執行緒的程式 裡,有兩種基本的資料:全域變數和區域變數。但在多執行緒程式裡,還有第三種資料類型:執行緒數據(TSD: Thread-Specific Data)。它和全域變數很象,在執行緒內部,各個函數可以象使用全域變數一樣調用它,但它對執行緒外部的其它執行緒是不可見的。這種資料的必要性是顯而易見 的。例如我們常見的變數errno,它返回標準的出錯資訊。它顯然不能是一個區域變數,幾乎每個函數都應該可以調用它;但它又不能是一個全域變數,否則在 A執行緒裡輸出的很可能是B執行緒的出錯資訊。要實現諸如此類的變數,我們就必須使用執行緒資料。我們為每個執行緒資料創建一個鍵,它和這個鍵相關聯,在各個執行緒 裡,都使用這個鍵來指代執行緒資料,但在不同的執行緒裡,這個鍵代表的資料是不同的,在同一個執行緒裡,它代表同樣的資料內容。
和執行緒資料相關的函數主要有4個:創建一個鍵;為一個鍵指定執行緒資料;從一個鍵讀取執行緒資料;刪除鍵。
創建鍵的函數原型為:
extern int pthread_key_create __P ((pthread_key_t *__key,void (*__destr_function) (void *)));
第一個參數為指向一個鍵值的指標,第二個參數指明了一個destructor函數,如果這個參數不為空,那麼當每個執行緒結束時,系統將調用這個函數來釋 放綁定在這個鍵上的區塊。這個函數常和函數pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,為了讓這個鍵只被創建一次。函數pthread_once聲明一個初始化函數,第一次調用pthread_once時它執行這 個函數,以後的調用將被它忽略。
在下面的例子中,我們創建一個鍵,並將它和某個資料相關聯。我們要定義一個函數 createWindow,這個函式定義一個圖形視窗(資料類型為Fl_Window *,這是圖形介面開發工具FLTK中的資料類型)。由於各個執行緒都會調用這個函數,所以我們使用執行緒資料。
/* 聲明一個鍵*/
pthread_key_t myWinKey;
/* 函數 createWindow */
void createWindow ( void ) {
Fl_Window * win;
static pthread_once_t once= PTHREAD_ONCE_INIT;
/* 調用函數createMyKey,創建鍵*/
pthread_once ( & once, createMyKey) ;
/*win指向一個新建立的視窗*/
win=new Fl_Window( 0, 0, 100, 100, "MyWindow");
/* 對此視窗作一些可能的設置工作,如大小、位置、名稱等*/
setWindow(win);
/* 將窗口指標值綁定在鍵myWinKey上*/
pthread_setpecific ( myWinKey, win);
}
/* 函數 createMyKey,創建一個鍵,並指定了destructor */
void createMyKey ( void ) {
pthread_keycreate(&myWinKey, freeWinKey);
}
/* 函數 freeWinKey,釋放空間*/
void freeWinKey ( Fl_Window * win){
delete win;
}
這樣,在不同的執行緒中調用函數createMyWin,都可以得到在執行緒內部均可見的視窗變數,這個變數通過函數 pthread_getspecific得到。在上面的例子中,我們已經使用了函數pthread_setspecific來將執行緒資料和一個鍵綁定在一 起。這兩個函數的原型如下:
extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));
extern void *pthread_getspecific __P ((pthread_key_t __key));
這兩個函數的參數意義和使用方法是顯而易見的。要注意的是,用pthread_setspecific為一個鍵指定新的執行緒資料時,必須自己釋放原有的 執行緒資料以回收空間。這個過程函數pthread_key_delete用來刪除一個鍵,這個鍵佔用的記憶體將被釋放,但同樣要注意的是,它只釋放鍵佔用的 記憶體,並不釋放該鍵關聯的執行緒資料所佔用的記憶體資源,而且它也不會觸發函數pthread_key_create中定義的destructor函數。執行緒 資料的釋放必須在釋放鍵之前完成。
1.4.2 2) 互斥鎖(Mutex)
互斥鎖用來保證一段時間內只有一個執行緒在執行一段代碼。
我們先看下面一段代碼。這是一個讀/寫程式,它們公用一個緩衝區,並且我們假定一個緩衝區只能保存一條資訊。即緩衝區只有兩個狀態:有資訊或沒有資訊。
void reader_function ( void );
void writer_function ( void );
char buffer;
int buffer_has_item=0;
pthread_mutex_t mutex;
struct timespec delay;
void main ( void ){
pthread_t reader;
/* 定義延遲時間*/
delay.tv_sec = 2;
delay.tv_nec = 0;
/* 用預設屬性初始化一個互斥鎖物件*/
pthread_mutex_init (&mutex,NULL);
pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL);
writer_function( );
}
void writer_function (void){
while(1){
/* 鎖定互斥鎖*/
pthread_mutex_lock (&mutex);
if (buffer_has_item==0){
buffer=make_new_item( );
buffer_has_item=1;
}
/* 打開互斥鎖*/
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}
void reader_function(void){
while(1){
pthread_mutex_lock(&mutex);
if(buffer_has_item==1){
consume_item(buffer);
buffer_has_item=0;
}
pthread_mutex_unlock(&mutex);
pthread_delay_np(&delay);
}
}
這裡聲明瞭互斥鎖變數mutex,結構pthread_mutex_t為不公開的資料類型,其中包含一個系統分配的屬性物件。函數 pthread_mutex_init用來生成一個互斥鎖。NULL參數表明使用預設屬性。如果需要聲明特定屬性的互斥鎖,須調用函數 pthread_mutexattr_init。函數pthread_mutexattr_setpshared和函數 pthread_mutexattr_settype用來設置互斥鎖屬性。
函數pthread_mutexattr_setpshared設置屬性pshared,它有兩個取值:
PTHREAD_PROCESS_PRIVATE //用來不同進程中的執行緒同步
PTHREAD_PROCESS_SHARED //用於同步本進程的不同執行緒
在上面的例子中,我們使用的是預設屬性PTHREAD_PROCESS_ PRIVATE。
函數pthread_mutexattr_settype用來設置互斥鎖類型,可選的類型有:
PTHREAD_MUTEX_NORMAL
PTHREAD_MUTEX_ERRORCHECK
PTHREAD_MUTEX_RECURSIVE
PTHREAD _MUTEX_DEFAULT
它們分別定義了不同的上鎖、解鎖機制,一般情況下,選用最後一個預設屬性。
pthread_mutex_lock聲明開始用互斥鎖上鎖,此後的代碼直至調用pthread_mutex_unlock為止,均被上鎖,即同一時間只 能被一個執行緒調用執行。當一個執行緒執行到pthread_mutex_lock處時,如果該鎖此時被另一個執行緒使用,那此執行緒被阻塞,即程式將等待到另一 個執行緒釋放此互斥鎖。在上面的例子中,我們使用了pthread_delay_np函數,讓執行緒睡眠一段時間,就是為了防止一個執行緒始終佔據此函數。
在使用互斥鎖的過程中很有可能會出現鎖死:兩個執行緒試圖同時佔用兩個資源,並按不同的次序鎖定相應的互斥鎖,例如兩個執行緒都需要鎖定互斥鎖1和互斥鎖 2,a執行緒先鎖定互斥鎖1,b執行緒先鎖定互斥鎖2,這時就出現了鎖死。此時我們可以使用函數 pthread_mutex_trylock,它是函數pthread_mutex_lock的非阻塞版本,當它發現鎖死不可避免時,它會返回相應的信 息,程式師可以針對鎖死做出相應的處理。另外不同的互斥鎖類型對鎖死的處理不一樣,但最主要的還是要程式師自己在程式設計注意這一點。
1.4.3 3) 條件變數(Condition)
前一節中我們講述了如何使用互斥鎖來實現執行緒間資料的共用和通信,互斥鎖一個明顯的缺點是它只有兩種狀態:鎖定和非鎖定。而條件變數通過允許執行緒阻塞和 等待另一個執行緒發送信號的方法彌補了互斥鎖的不足,它常和互斥鎖一起使用。使用時,條件變數被用來阻塞一個執行緒,當條件不滿足時,執行緒往往解開相應的互斥 鎖並等待條件發生變化。一旦其它的某個執行緒改變了條件變數,它將通知相應的條件變數喚醒一個或多個正被此條件變數阻塞的執行緒。這些執行緒將重新鎖定互斥鎖並 重新測試條件是否滿足。一般說來,條件變數被用來進行線承間的同步。
條件變數的結構為pthread_cond_t,函數pthread_cond_init()被用來初始化一個條件變數。它的原型為:
extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));
其中cond是一個指向結構pthread_cond_t的指標,cond_attr是一個指向結構pthread_condattr_t的指標。結構 pthread_condattr_t是條件變數的屬性結構,和互斥鎖一樣我們可以用它來設置條件變數是進程內可用還是進程間可用,預設值是 PTHREAD_ PROCESS_PRIVATE,即此條件變數被同一進程內的各個執行緒使用。注意初始化條件變數只有未被使用時才能重新初始化或被釋放。釋放一個條件變數 的函數為pthread_cond_ destroy(pthread_cond_t cond)。
函數pthread_cond_wait()使執行緒阻塞在一個條件變數上。它的函數原型為:
extern int pthread_cond_wait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex));
執行緒解開mutex指向的鎖並被條件變數cond阻塞。執行緒可以被函數pthread_cond_signal和函數 pthread_cond_broadcast喚醒,但是要注意的是,條件變數只是起阻塞和喚醒執行緒的作用,具體的判斷條件還需用戶給出,例如一個變數是 否為0等等,這一點我們從後面的例子中可以看到。執行緒被喚醒後,它將重新檢查判斷條件是否滿足,如果還不滿足,一般說來執行緒應該仍阻塞在這裡,被等待被下 一次喚醒。這個過程一般用while語句實現。
另一個用來阻塞執行緒的函數是pthread_cond_timedwait(),它的原型為:
extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex, __const struct timespec *__abstime));
它比函數pthread_cond_wait()多了一個時間參數,經歷abstime段時間後,即使條件變數不滿足,阻塞也被解除。
函數pthread_cond_signal()的原型為:
extern int pthread_cond_signal __P ((pthread_cond_t *__cond));
它用來釋放被阻塞在條件變數cond上的一個執行緒。多個執行緒阻塞在此條件變數上時,哪一個執行緒被喚醒是由執行緒的調度策略所決定的。要注意的是,必須用保 護條件變數的互斥鎖來保護這個函數,否則條件滿足信號又可能在測試條件和調用pthread_cond_wait函數之間被發出,從而造成無限制的等待。 下面是使用函數pthread_cond_wait()和函數pthread_cond_signal()的一個簡單的例子。
pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count;
decrement_count () {
pthread_mutex_lock (&count_lock);
while(count==0)
pthread_cond_wait( &count_nonzero, &count_lock);
count=count -1;
pthread_mutex_unlock (&count_lock);
}
increment_count(){
pthread_mutex_lock(&count_lock);
if(count==0)
pthread_cond_signal(&count_nonzero);
count=count+1;
pthread_mutex_unlock(&count_lock);
}
count值為0時, decrement函數在pthread_cond_wait處被阻塞,並打開互斥鎖count_lock。此時,當調用到函數 increment_count時,pthread_cond_signal()函數改變條件變數,告知decrement_count()停止阻塞。讀 者可以試著讓兩個執行緒分別運行這兩個函數,看看會出現什麼樣的結果。
函數pthread_cond_broadcast(pthread_cond_t *cond)用來喚醒所有被阻塞在條件變數cond上的執行緒。這些執行緒被喚醒後將再次競爭相應的互斥鎖,所以必須小心使用這個函數。
1.4.4 4) 信號量(Semaphore)
信號量本質上是一個非負的整數計數器,它被用來控制對公共資源的訪問。當公共資源增加時,調用函數sem_post()增加信號量。只有當信號量值大於 0時,才能使用公共資源,使用後,函數sem_wait()減少信號量。函數sem_trywait()和函數pthread_ mutex_trylock()起同樣的作用,它是函數sem_wait()的非阻塞版本。下面我們逐個介紹和信號量有關的一些函數,它們都在標頭檔 /usr/include/semaphore.h中定義。
信號量的資料類型為結構sem_t,它本質上是一個長整型的數。函數sem_init()用來初始化一個信號量。它的原型為:
extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
sem為指向信號量結構的一個指標;pshared不為0時此信號量在進程間共用,否則只能為當前進程的所有執行緒共用;value給出了信號量的初始值。
函數sem_post( sem_t *sem )用來增加信號量的值。當有執行緒阻塞在這個信號量上時,調用這個函數會使其中的一個執行緒不在阻塞,選擇機制同樣是由執行緒的調度策略決定的。
函數sem_wait( sem_t *sem )被用來阻塞當前執行緒直到信號量sem的值大於0,解除阻塞後將sem的值減一,表明公共資源經使用後減少。函數sem_trywait ( sem_t *sem )是函數sem_wait()的非阻塞版本,它直接將信號量sem的值減一。
函數sem_destroy(sem_t *sem)用來釋放信號量sem。
下面我們來看一個使用信號量的例子。在這個例子中,一共有4個執行緒,其中兩個執行緒負責從檔讀取資料到公共的緩衝區,另兩個執行緒從緩衝區讀取資料作不同的處理(加和乘運算)。
/* File sem.c */
#include
#include
#include
#define MAXSTACK 100
int stack[MAXSTACK][2];
int size=0;
sem_t sem;
/* 從文件1.dat讀取資料,每讀一次,信號量加一*/
void ReadData1(void){
FILE *fp=fopen("1.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/*從文件2.dat讀取數據*/
void ReadData2(void){
FILE *fp=fopen("2.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
sem_post(&sem);
++size;
}
fclose(fp);
}
/*阻塞等待緩衝區有資料,讀取資料後,釋放空間,繼續等待*/
void HandleData1(void){
while(1){
sem_wait(&sem);
printf("Plus:%d+%d=%d\n",stack[size][0],stack[size][1],
stack[size][0]+stack[size][1]);
--size;
}
}
void HandleData2(void){
while(1){
sem_wait(&sem);
printf("Multiply:%d*%d=%d\n",stack[size][0],stack[size][1],
stack[size][0]*stack[size][1]);
--size;
}
}
int main(void){
pthread_t t1,t2,t3,t4;
sem_init(&sem,0,0);
pthread_create(&t1,NULL,(void *)HandleData1,NULL);
pthread_create(&t2,NULL,(void *)HandleData2,NULL);
pthread_create(&t3,NULL,(void *)ReadData1,NULL);
pthread_create(&t4,NULL,(void *)ReadData2,NULL);
/* 防止程式過早退出,讓它在此無限期等待*/
pthread_join(t1,NULL);
}
在Linux下,我們用命令gcc -lpthread sem.c -o sem生成可執行檔sem。 我們事先編輯好資料檔案1.dat和2.dat,假設它們的內容分別為1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,我們運行sem,得到如下的結果:
Multiply:-1*-2=2
Plus:-1+-2=-3
Multiply:9*10=90
Plus:-9+-10=-19
Multiply:-7*-8=56
Plus:-5+-6=-11
Multiply:-3*-4=12
Plus:9+10=19
Plus:7+8=15
Plus:5+6=11
從中我們可以看出各個執行緒間的競爭關係。而數值並未按我們原先的順序顯示出來這是由size這個數值被各個執行緒任意修改的緣故。這也往往是多執行緒程式設計要注意的問題。
1.4.5 5)非同步信號
由 於LinuxThreads是在核外使用核內羽量級進程實現的執行緒,所以基於內核的非同步信號操作對於執行緒也是有效的。但同時,由於非同步信號總是實際發往某 個進程,所以無法實現POSIX標準所要求的"信號到達某個進程,然後再由該進程將信號分發到所有沒有阻塞該信號的執行緒中"原語,而是只能影響到其中一個 執行緒。
POSIX非同步信號同時也是一個標準C庫提供的功能,主要包括信號集管理(sigemptyset()、sigfillset()、 sigaddset()、sigdelset()、sigismember()等)、信號處理函數安裝(sigaction())、信號阻塞控制 (sigprocmask())、被阻塞信號查詢(sigpending())、信號等待(sigsuspend())等,它們與發送信號的kill() 等函數配合就能實現進程間非同步信號功能。LinuxThreads圍繞執行緒封裝了sigaction()何raise(),本節集中討論 LinuxThreads中擴展的非同步信號函數,包括pthread_sigmask()、pthread_kill()和sigwait()三個函數。 毫無疑問,所有POSIX非同步信號函數對於執行緒都是可用的。
int pthread_sigmask(int how, const sigset_t *newmask, sigset_t *oldmask)
設置執行緒的信號遮罩碼,語義與sigprocmask()相同,但對不允許遮罩的Cancel信號和不允許回應的Restart信號進行了保護。被遮罩的信號保存在信號佇列中,可由sigpending()函數取出。
int pthread_kill(pthread_t thread, int signo)
向thread號執行緒發送signo信號。實現中在通過thread執行緒號定位到對應進程號以後使用kill()系統調用完成發送。
int sigwait(const sigset_t *set, int *sig)
掛 起執行緒,等待set中指定的信號之一到達,並將到達的信號存入*sig中。POSIX標準建議在調用sigwait()等待信號以前,進程中所有執行緒都應 遮罩該信號,以保證僅有sigwait()的調用者獲得該信號,因此,對於需要等待同步的非同步信號,總是應該在創建任何執行緒以前調用 pthread_sigmask()遮罩該信號的處理。而且,調用sigwait()期間,原來附接在該信號上的信號處理函數不會被調用。
如果在等待期間接收到Cancel信號,則立即退出等待,也就是說sigwait()被實現為取消點。
1.4.6 6)關於執行緒的撤銷
在多執行緒中,某些情況下可能需要撤銷一個執行緒,恢復執行緒修改了的一些變數,並且釋放執行緒佔用的一些共用資源,將系統返回到這個執行緒執行的初始狀態或特定狀 態。在處理執行緒撤銷時需要小心處理。不能遺留已上鎖的交互鎖(這會導致鎖死),也不能留下末釋放的記憶體區(這將導致記憶體洩漏)。
pthread提供了撤銷執行緒的相關介面函數,這些介面可用於允許或者禁止撤銷一個執行緒,設定撤銷點等。
調用帶有一個執行緒ID作為參數的函數pthread_cancel()可以撤銷一個執行緒。函數pthread_setcancelstate()和 pthread_setcanceltype()用來設定目標執行緒的狀態和屬性值,這些值確定執行緒如何回應撤銷請求。
pthread_setcancelstate()可以將執行緒設置為PTHREAD-CANCEL-ENABLE或者PTHREAD-CANCEL- DISABLE兩種狀態。如果執行緒的狀態是PTHREAD-CANCEL-DISABLE,那麼所有對那個執行緒的撤銷請求將被掛起。如果狀態為 PTHREAD-CANCEL-ENABLE,那麼撤銷行為依賴於這個執行緒的撤銷類型。執行緒的撤銷類型由函數pthread_setcanceltype ()設定。
pthread_setcanceltype()可以將執行緒的撤銷類型設置為PTHREAD-CANCEL-DEFERRED或者PTHREAD- CANCEL-ASYNCHRONOUS。如果撤銷類型是PTHREAD-CANCEL-ASYNCHRONOUS,接受到一個 pthread_cancel()調用後,執行緒會立即撤銷。如果撤銷類型是PTHREAD_CANCEL-DEFERRED,那麼直到執行緒達到一個撤銷點 時才發生撤銷。
通過在執行緒代碼中插入函式呼叫pthread_testcancel()就可以建立一個撤銷點。當此函數執行時,如果掛起一個撤銷,那麼pthread_testcancel()將不返回。
除了使用pthread_testcancel()外,pthreads還定義一些撤銷點。包括等待在pthread_cond_timedwait() 和pthread_cond_wait()上的執行緒、等待在pthread_join()中的另一執行緒結束的執行緒、阻塞在sigwait()上的執行緒。也 有許多可作為撤銷點的標準庫調用。通常這些函數會使執行緒在它上面阻塞。
pthread庫提供了一對pthread_cleanup_push()/pthread_cleanup_pop()函數對用於自動釋放資源--從 pthread_cleanup_push()的調用點到pthread_cleanup_pop()之間的程式段中的終止動作(包括調用 pthread_exit()和取消點終止)都將執行pthread_cleanup_push()所指定的清理函數。兩個函式定義如下:
void pthread_cleanup_push(void (*routine) (void *), void *arg);
void pthread_cleanup_pop(int execute);
這 兩個函數採用先入後出的棧結構管理,void routine(void *arg)函數在調用pthread_cleanup_push()時壓入清理函數棧,多次對pthread_cleanup_push()的調用將在清 理函數棧中形成一個函數鏈,在執行該函數鏈時按照壓棧的相反順序彈出。execute參數表示執行到pthread_cleanup_pop()時是否在 彈出清理函數的同時執行該函數,為0表示不執行,非0為執行;這個參數並不影響異常終止時清理函數的執行。
pthread_cleanup_push()/pthread_cleanup_pop()是以巨集方式實現的,這是pthread.h中的巨集定義:
#define pthread_cleanup_push(routine,arg) \
{ struct _pthread_cleanup_buffer _buffer; _pthread_cleanup_push (&_buffer, (routine), (arg));
#define pthread_cleanup_pop(execute) _pthread_cleanup_pop (&_buffer, (execute)); }
可 見,pthread_cleanup_push()帶有一個"",因此這兩個函數必須成對出現,且必須位於程式的同一級別的程式碼片段中才能通過編譯。在下面 的例子裡,當執行緒在"do some work"中終止時,將主動調用pthread_mutex_unlock(mut),以完成解鎖動作。
pthread_cleanup_push(pthread_mutex_unlock, (void *) &mut);
pthread_mutex_lock(&mut);
/* do some work */
pthread_mutex_unlock(&mut);
pthread_cleanup_pop(0);
必 須要注意的是,如果執行緒處於PTHREAD_CANCEL_ASYNCHRONOUS狀態,上述程式碼片段就有可能出錯,因為CANCEL事件有可能在 pthread_cleanup_push()和pthread_mutex_lock()之間發生,或者在pthread_mutex_unlock ()和pthread_cleanup_pop()之間發生,從而導致清理函數unlock一個並沒有加鎖的mutex變數,造成錯誤。因此,在使用清理 函數的時候,都應該暫時設置成PTHREAD_CANCEL_DEFERRED模式。為此,POSIX的Linux實現中還提供了一對不保證可移植的擴展 函數:
pthread_cleanup_push_defer_np()
pthread_cleanup_pop_defer_np()
功能與以下程式碼片段相當:
{
int oldtype;
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &oldtype);
pthread_cleanup_push(routine, arg);
...
pthread_cleanup_pop(execute);
pthread_setcanceltype(oldtype, NULL);
}
1.5 小結
多執行緒程式設計是一個很有意思也很有用的技術,使用多執行緒技術的網路螞蟻是目前最常用的下載工具之一,使用多執行緒技術的grep比單執行緒的grep要快上幾倍,類似的例子還有很多。希望大家能用多執行緒技術寫出高效實用的好程式來。
2 同步和非同步與阻塞和非阻塞的區別
http://blog.huanghao.me/?p=95
發現我好像越來越喜歡糾纏於這些定義=_=。
同步和非同步與阻塞與非阻塞是在通信和I/O中常用的字眼,之前在許多地方同步與阻塞,非同步與非阻塞常常被混為一談,帶來了許多混亂,其實同步、非同步和阻塞、非阻塞是兩個不同的概念。最近隨著非同步IO(AIO)越來越多的應用,對這兩個概念進行區分和解釋的文章也越來越多,但是問起身邊的同學,能說清楚的倒也不多,所以我就順便跟風寫一篇科普文吧(越來越水了=_=)。
2.1 同步(synchronous)和非同步(asynchronous)
同步(synchronous)和非同步(asynchronous)其實是針對消息的發送和接受的次序而言的(在通信中就是消息的發送和接收,在IO中就是資料的讀和寫)。同步的意思就是消息的發送和接收是有序的,即接收和發送第二個包一定在第一個包之後第三個包之前,而不是亂序。非同步的意思就是消息的發送和接收是可以亂序的,第一個包沒發完可以直接發第二個包。
2.2 阻塞(block)和非阻塞(non-block)
至於阻塞(block)和非阻塞(non-block)其實描述的是進程或執行緒進行等待時的一種方式。阻塞的意思是等待時進程或執行緒需要掛起,而非阻塞則是等待時執行緒或進程不需要被掛起,不影響執行緒的執行,這時執行緒或進程可以繼續處理其它事物,不因為這個等待而受到影響(當然它仍然在等待這個消息,只不過可能會在執行緒或進程執行週期的某一個地方去查看消息的通知,而不是立即在原地等待)。
舉個例子,兩個人之間發短信,最簡單的就是同步阻塞的方式,一個人發短信,然後啥也不幹地等在手機前面,直到對方回信,接下來才發第二條短信(這時也確認了第一條短信已發到)。而同步非阻塞方式也就是大家常用的方式,則是發出去消息,然後去幹別的事,(體現了非阻塞)等對方回短信之後(相當於確認了第一條短信已收到,並且有後續資料過來),再發第二條短信(體現了同步)。非同步阻塞的方式,則是一口氣發出幾十條短信(由於中國移動並不保證發出短信的先後順序,可能導致對方收到短信的順序和發出去時不一致,這就體現了非同步的概念,而且理論上發信的順序也可以是亂的),發完之後就啥也不幹,等對方一條一條的回信(這體現了阻塞的概念)。而如果在一口氣發出幾十條短信後沒有傻傻的等待,而是去別的地方玩去了,對方的回信到一條讀一條,則就變成非同步非阻塞的方式了。
不知道通過上面的例子,大家是不是已經可以理解這兩組概念之間的區別了。這裡有篇相關的文章寫得不錯,如果還有些不理解的,可以再去閱讀一下。由於國內在IT領域的起步落後國外(主要是美國)一些年份,再加上互聯網的迅速普及,導致許多以訛傳訛的現象時有發生。這兩組本來適用範圍並不相同的概念卻在很長一段時間內被混為一談,應該就是這方面的例子。這種錯誤增加了大家的學習成本,也不利於在某一些領域的進一步研究,所以個人以為搞清楚這些概念還是很有必要的(最後為自己的又一篇水文開脫一下=_=)
3 pthread_join和pthread_detach的用法
http://huoyj.iteye.com/blog/1907529
阻塞(block)和非阻塞(non-block)其實描述的是進程或線程進行等待時的一種方式。
阻塞的意思是等待時進程或線程需要掛起。
掛起,
是指在作業系統行程管理將前臺的行程暫停並轉入後台的動作。將行程掛起可以讓使用者在前臺執行其他的行程。掛起的行程通常釋放除CPU以外已經佔有的系統資源,如記憶體等)。
3.1 一:關於join
join
join是三種同步執行緒的方式之一。另外兩種分別是互斥鎖(mutex)和條件變數(condition variable)。
調用pthread_join()將阻塞自己,一直到要等待加入的執行緒運行結束。
可以用pthread_join()獲取執行緒的返回值。
一個執行緒對應一個pthread_join()調用,對同一個執行緒進行多次pthread_join()調用是邏輯錯誤。
join or detach
執行緒分兩種:一種可以join,另一種不可以。該屬性在創建執行緒的時候指定。
joinable執行緒可在創建後,用pthread_detach()顯式地分離。但分離後不可以再合併。該操作不可逆。
為了確保移植性,在創建執行緒時,最好顯式指定其join或detach屬性。似乎不是所有POSIX實現都是用joinable作默認。
3.2 二: pthread_detach
創建一個執行緒預設的狀態是joinable, 如果一個執行緒結束運行但沒有被join,則它的狀態類似於進程中的Zombie Process,即還有一部分資源沒有被回收(退出狀態碼),所以創建執行緒者應該調用pthread_join來等待中的執行緒運行結束,並可得到執行緒的退出代碼,回收其資源(類似於wait,waitpid)
但是調用pthread_join(pthread_id)後,如果該執行緒沒有運行結束,調用者會被阻塞,在有些情況下我們並不希望如此,比如在Web伺服器中當主執行緒為每個新來的連結創建一個子執行緒進行處理的時候,主執行緒並不希望因為調用pthread_join而阻塞(因為還要繼續處理之後到來的連結),這時可以在子執行緒中加入代碼
pthread_detach(pthread_self())
或者父執行緒調用
pthread_detach(thread_id)(非阻塞,可立即返回)
這將該子執行緒的狀態設置為detached,則該執行緒運行結束後會自動釋放所有資源。
3.3 三:pthread_join
調用pthread_join的執行緒會阻塞,直到指定的執行緒返回,調用了pthread_exit,或者被取消。
如果執行緒簡單的返回,那麼rval_ptr被設置成執行緒的返回值,參見範例1;如果調用了pthread_exit,則可將一個無類型指標返回,在pthread_join中對其進行訪問,參見範例2;如果執行緒被取消,rval_ptr被設置成PTHREAD_CANCELED。
如果我們不關心執行緒的返回值,那麼我們可以把rval_ptr設置為NULL。
範例1:
#include <pthread.h>
#include <string.h>
void *thr_fn1(void *arg)
{
printf(“thread 1 returning.\n”);
return((void *)1);
}
void *thr_fn2(void *arg)
{
printf(“thread 2 exiting.\n”);
return((void *)2);
}
int main()
{
pthread_t tid1,tid2;
void *tret;
pthread_create(&tid1,NULL,thr_fn1,NULL);
pthread_create(&tid2,NULL,thr_fn2,NULL);
pthread_join(tid1,&tret);
printf(“thread 1 exit code %d\n”,(int)tret);
pthread_join(tid2,&tret);
printf(“thread 2 exit code %d\n”,(int)tret);
exit(0);
}
運行結果:
thread 1 returning.
thread 1 exit code 1.
thread 2 exiting.
thread 2 exit code 2.
範例2:
#include <stdio.h>
#include <pthread.h>
void thread1(char s[])
{
printf("This is a pthread1.\n");
printf("%s\n",s);
pthread_exit("Hello first!"); //結束執行緒,返回一個值。
}
void thread2(char s[])
{
printf("This is a pthread2.\n");
printf("%s\n",s);
pthread_exit("Hello second!");
}
int main(void)
{
pthread_t id1,id2;
void *a1,*a2;
int i,ret1,ret2;
char s1[]="This is first thread!";
char s2[]="This is second thread!";
ret1=pthread_create(&id1,NULL,(void *) thread1,s1);
ret2=pthread_create(&id2,NULL,(void *) thread2,s2);
if(ret1!=0){
printf ("Create pthread1 error!\n");
exit (1);
}
pthread_join(id1,&a1);
printf("%s\n",(char*)a1);
if(ret2!=0){
printf ("Create pthread2 error!\n");
exit (1);
}
printf("This is the main process.\n");
pthread_join(id2,&a2);
printf("%s\n",(char*)a2);
return (0);
}
運行結果:
[****@XD**** c]$ ./example
This is a pthread1.
This is first thread!
Hello first!
This is the main process.
This is a pthread2.
3.4 <參考資料語>
一般情況下,進程中各個執行緒的運行都是相互獨立的,執行緒的終止並不會通知,也不會影響其他執行緒,終止的執行緒所佔用的資源也並不會隨著執行緒的終止而得到釋 放。正如進程之間可以用wait()系統調用來同步終止並釋放資源一樣,執行緒之間也有類似機制,那就是pthread_join()函數
pthread_join()的調用者將掛起並等待th執行緒終止,retval是pthread_exit()調用者執行緒(執行緒ID為th)的返回值,如 果thread_return不為NULL,則*thread_return=retval。需要注意的是一個執行緒僅允許唯一的一個執行緒使用 pthread_join()等待它的終止,並且被等待的執行緒應該處於可join狀態,即非DETACHED狀態
如果進程中的某個執行緒執行了pthread_detach(th),則th執行緒將處於DETACHED狀態,這使得th執行緒在結束運行時自行釋放所佔用的 記憶體資源,同時也無法由pthread_join()同步,pthread_detach()執行之後,對th請求pthread_join()將返回錯誤
一個可join的執行緒所佔用的記憶體僅當有執行緒對其執行了pthread_join()後才會釋放,因此為了避免記憶體洩漏,所有執行緒的終止,要麼已設為DETACHED,要麼就需要使用pthread_join()來回收
3) 主執行緒用pthread_exit還是return
用pthread_exit只會使主執行緒自身退出,產生的子執行緒繼續執行;用return則所有執行緒退出。
綜合以上要想讓子執行緒總能完整執行(不會中途退出),一種方法是在主執行緒中調用pthread_join對其等待,即pthread_create/pthread_join/pthread_exit或return;一種方法是在主執行緒退出時使用pthread_exit,這樣子執行緒能繼續執行,即pthread_create/pthread_detach/pthread_exit;還有一種是pthread_create/pthread_detach/return,這時就要保證主執行緒不能退出,至少是子執行緒完成前不能退出。現在的項目中用的就是第三種方法,主執行緒是一個閉環,子執行緒有的是閉環有的不是。
3.5 <參考資料語>
理論上說,pthread_exit()和執行緒宿體函數退出的功能是相同的,函數結束時會在內部自動調用pthread_exit()來清理執行緒相關的資源。但實際上二者由於編譯器的處理有很大的不同。
在進程主函數(main())中調用pthread_exit(),只會使主函數所在的執行緒(可以說是進程的主執行緒)退出;而如果是return,編譯器將使其調用進程退出的代碼(如_exit()),從而導致進程及其所有執行緒結束運行。
1 MULTI-THREADED PROGRAMMING III - C/C++ CLASS THREAD FOR PTHREADS - 2013
1 Reference
[1] http://www.codeproject.com/Articles/21114/Creating-a-C-Thread-Class
[2]http://learn.akae.cn/media/ch35.html
[3]https://sites.google.com/site/myembededlife/Home/applications--development/linux-multi-thread-programming
[4] IBM Posix執行緒http://www.ibm.com/developerworks/cn/linux/theme/posix_thread/index.html
[6]http://www.yolinux.com/TUTORIALS/LinuxTutorialPosixThreads.html#BASICS
[5]http://www.bogotobogo.com/cplusplus/multithreading_pthread.php
[2]http://learn.akae.cn/media/ch35.html
[3]https://sites.google.com/site/myembededlife/Home/applications--development/linux-multi-thread-programming
[4] IBM Posix執行緒http://www.ibm.com/developerworks/cn/linux/theme/posix_thread/index.html
[6]http://www.yolinux.com/TUTORIALS/LinuxTutorialPosixThreads.html#BASICS
[5]http://www.bogotobogo.com/cplusplus/multithreading_pthread.php