(四十一)執行緒——執行緒原語
一、準備工作
檢視manpage關於pthread的函式
man -k pthread
安裝pthread相關manpage
sudo apt-get install manpages-posix manpages-posix-dev
二、pthread_create
建立執行緒
#include <pthread.h>
int pthread_create( pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *) ,
void *arg);
pthread_t *thread:傳遞一個pthread_t變數地址進來,用於儲存新執行緒的tid(執行緒ID)
const pthread_attr_t *attr:執行緒屬性設定(eg:執行緒的優先順序、執行緒的佔空間大小),如使用預設屬性,則傳NULL
void *(*start_routine) (void *):函式指標,指向新執行緒應該載入執行的函式模組
void *arg:指定執行緒將要載入呼叫的那個函式的引數
返回值:成功返回0,失敗返回錯誤號。
以前學過的系統函式都是成功返回0,失敗返回-1,錯誤號儲存在全域性變數errno中,
而pthread庫的函式都是通過返回值返回錯誤號,雖然每個執行緒也都有一個errno,
但這是為了相容其它函式介面而提供的,pthread庫本身並不使用它,通過返回值返回錯誤碼更加清晰。
其中:
Compile and link with -lpthread.(即連結的時候要加-lpthread引數)
typedef unsigned long int pthread_t;
在一個執行緒中呼叫pthread_create()建立新的執行緒後,當前執行緒從pthread_create()返回繼續往下執行,而新的執行緒所執行的程式碼由我們傳給pthread_create的函式指標start_routine決定。start_routine函式接收一個引數,是通過pthread_create的arg引數傳遞給它的,該引數的型別為void *,這個指標按什麼型別解釋由呼叫者自己定義。start_routine的返回值型別也是void *,這個指標的含義同樣由呼叫者自己定義
pthread_create成功返回後,新建立的執行緒的id被填寫到thread引數所指向的記憶體單元。我們知道程序id的型別是pid_t,每個程序的id在整個系統中是唯一的,呼叫getpid()可以獲得當前程序的id,是一個正整數值。執行緒id的型別是thread_t,它只在當前程序中保證是唯一的,在不同的系統中thread_t這個型別有不同的實現,它可能是一個整數值,也可能是一個結構體,也可能是一個地址,所以不能簡單地當成整數用printf列印,呼叫pthread_self()可以獲得當前執行緒的id。
attr引數表示執行緒屬性,本節不深入討論執行緒屬性(下節簡單討論),所有程式碼例子都傳NULL給attr引數,表示執行緒屬性取預設值,感興趣的讀者可以參考[APUE2e]。
三、pthread_self
函式作用:獲取呼叫執行緒tid
#include <pthread.h>
pthread_t pthread_self(void);
接下來看一個簡單的例子(pthread_create和pthread_self使用)
#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;
}
由於pthread_create的錯誤碼不儲存在errno中,因此不能直接用perror()列印錯誤資訊,可以先用strerror()把錯誤碼轉換成錯誤資訊再列印。
如果任意一個執行緒呼叫了exit或_exit,則整個程序的所有執行緒都終止,由於從main函式return也相當於呼叫exit,為了防止新建立的執行緒還沒有得到執行就終止,我們在main函式return之前延時1秒,這只是一種權宜之計,即使主執行緒等待1秒,核心也不一定會排程新建立的執行緒執行,下一節我們會看到更好的辦法。
(注:在這裡我們可以思考一個情況,pthread_self獲得的tid和pthread_create函式裡得到tid是否會出現不一致呢?看以下程式碼)
#include <pthread.h>
#include <stdio.h>
void *th_fun(void *arg)
{
int *p = (int *)arg;
printf("thread PID = %d\n", getpid());
printf("thread ID = %x\n",(unsigned int)pthread_self());
printf("thread *arg = %d\n", *p);
sleep(1);//為防止下述例外產生
}
int main(void)
{
pthread_t tid;
int n = 10;
//pthread_create執行過程: 1.建立執行緒 2.將執行緒號填寫到tid 3.返回函式呼叫
pthread_create(&tid, NULL, th_fun, (void *)&n);
/*主執行緒中的pthread_create 返回值tid == 子執行緒中pthread_self()
*但是有例外:
* 若在上訴過程1與2之間cpu的時間片被該執行緒佔用
* 而建立的執行緒執行完畢後退出
* 則主執行緒中的pthread_create 返回值tid為垃圾值
* 不等於子執行緒中pthread_self()
*/
printf("main PID = %d\n", getpid());
printf("main thread ID = %x\n",(unsigned int)pthread_self());
printf("main created thread = %x\n", (unsigned int)tid);
sleep(2);//等待子執行緒完成,當主執行緒退出後,則該程序退出,子執行緒全部被回收
return 0;
}
四、pthread_exit
pthread_exit為呼叫執行緒退出函式,注意和exit函式的區別,任何執行緒裡exit導致程序退出,其他執行緒未工作結束,主控執行緒退出時不能return或exit。
需要注意,pthread_exit或者return返回的指標所指向的記憶體單元必須是全域性的或者是用malloc分配的,不能線上程函式的棧上分配,因為當其它執行緒得到這個返回指標時執行緒函式已經退出了。
(注:如果在子執行緒中呼叫該函式,則子執行緒退出,如果在主執行緒中呼叫該函式則主執行緒會成為殭屍執行緒,然後等待所有子執行緒都退出之後再退出該執行緒)
#include <pthread.h>
void pthread_exit(void *retval);
void *retval:執行緒退出時傳遞出的引數,可以是退出值或地址,如是地址時,不能是執行緒內部申請的區域性地址。
五、pthread_join
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
pthread_t thread:回收執行緒的tid
void **retval:接收退出執行緒傳遞出的返回值
返回值:成功返回0,失敗返回錯誤號
呼叫該函式的執行緒將掛起等待,直到id為thread的執行緒終止。thread執行緒以不同的方法終止,通過pthread_join得到的終止狀態是不同的,總結如下:
如果thread執行緒通過return返回,retval所指向的單元裡存放的是thread執行緒函式的返回值。
如果thread執行緒被別的執行緒呼叫pthread_cancel異常終止掉,retval所指向的單元裡存放的是常數PTHREAD_CANCELED。
如果thread執行緒是自己呼叫pthread_exit終止的,retval所指向的單元存放的是傳給pthread_exit的引數。
如果對thread執行緒的終止狀態不感興趣,可以傳NULL給retval引數。
六、pthread_cancel
在程序內某個執行緒可以取消另一個執行緒。
#include <pthread.h>
int pthread_cancel(pthread_t thread);
被取消的執行緒,退出值,定義在Linux的pthread庫中,常數THREAD_CANCELED的值是-1。
可以在標頭檔案pthread.h中找到它的定義:
#define PTHREAD_CANCELED ((void *) -1)
例子:(pthread_join、pthread_exit和pthread_cancel的使用)
#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);//在這裡的效果和上面的return一樣
}
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);
pthread_join(tid, &tret);//阻塞回收
printf("thread 1 exit code %d\n", (int)tret);
pthread_create(&tid, NULL, thr_fn2, NULL);
pthread_join(tid, &tret);
printf("thread 2 exit code %d\n", (int)tret);
pthread_create(&tid, NULL, thr_fn3, NULL);
sleep(3);
pthread_cancel(tid);//關閉執行緒
pthread_join(tid, &tret);//回收
printf("thread 3 exit code %d\n", (int)tret);
return 0;
}
七、pthread_detach
#include <pthread.h>
int pthread_detach(pthread_t tid);
pthread_t tid:要被分的離執行緒的tid
返回值:成功返回0,失敗返回錯誤號。
一般情況下,執行緒終止後,其終止狀態一直保留到其它執行緒呼叫pthread_join獲取它的狀態為止。但是執行緒也可以被置為detach狀態,這樣的執行緒一旦終止就立刻回收它佔用的所有資源,而不保留終止狀態。不能對一個已經處於detach狀態的執行緒呼叫pthread_join,這樣的呼叫將返回EINVAL。如果已經對一個執行緒呼叫了pthread_detach就不能再呼叫pthread_join了。
例子:(pthread_detach的使用)
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
void *thr_fn(void *arg)
{
int n = 3;
while (n--) {
printf("thread count %d\n", n);
sleep(1);
}
return (void *)1;
}
int main(void)
{
pthread_t tid;
void *tret;
int err;
pthread_create(&tid, NULL, thr_fn, NULL);
//第一次執行時註釋掉下面這行,第二次再開啟,分析兩次結果
pthread_detach(tid);//設定成分離態
while (1)
{
err = pthread_join(tid, &tret);
if (err != 0)//呼叫不成功,打印出資訊
fprintf(stderr, "thread %s\n", strerror(err));
else
fprintf(stderr, "thread exit code %d\n", (int)tret);
sleep(1);
}
return 0;
}
當我們不關係執行緒的返回值的時候一般呼叫該函式做收尾處理,十分方便!
八、執行緒終止方式
如果需要只終止某個執行緒而不終止整個程序,可以有三種方法:
1.從執行緒主函式return。這種方法對主控執行緒不適用,從main函式return相當於呼叫exit。
2.一個執行緒可以呼叫pthread_cancel終止同一程序中的另一個執行緒。
3.執行緒可以呼叫pthread_exit終止自己。
同一程序的執行緒間,pthread_cancel向另一執行緒發終止訊號。系統並不會馬上關閉被取消執行緒,只有在被取消執行緒下次系統呼叫時,才會真正結束執行緒。或呼叫pthread_testcancel,讓核心去檢測是否需要取消當前執行緒
pthread_cancel函式需要注意的地方:
#include <pthread.h>
#include <stdio.h>
void *th_fun(void *arg)
{
int i = 0;
int *p = (int *)arg;
printf("thread PID = %d\n", getpid());
printf("thread ID = %x\n",(unsigned int)pthread_self());
printf("thread *arg = %d\n", *p);
while(1)
{
i++;
}
}
int main(void)
{
pthread_t tid;
int n = 10;
//過程: 1.建立執行緒 2.將執行緒號填寫到tid 3.返回函式呼叫
pthread_create(&tid, NULL, th_fun, (void *)&n);
printf("main PID = %d\n", getpid());
printf("main thread ID = %x\n",(unsigned int)pthread_self());
printf("main created thread = %x\n", (unsigned int)tid);
pthread_cancel(tid);
// 同一程序的執行緒間,才能使用pthread_cancel向另一執行緒發終止訊號
// 系統並不會馬上關閉被取消執行緒,只有在被取消執行緒下次系統呼叫時,才會真正結束執行緒
// 執行後通過ps -eLf 和 ps -Lw pid可以檢視
while(1)
sleep(5);
return 0;
}
對於上述情況,使用pthread_testcancel函式進行修正:
#include <pthread.h>
#include <stdio.h>
void *th_fun(void *arg)
{
int i = 0;
int *p = (int *)arg;
printf("thread PID = %d\n", getpid());
printf("thread ID = %x\n",(unsigned int)pthread_self());
printf("thread *arg = %d\n", *p);
while(1)
{
i++;
pthread_testcancel();
//呼叫pthread_testcancel,讓核心去檢測是否需要取消當前執行緒
}
}
int main(void)
{
pthread_t tid;
int n = 10;
//過程: 1.建立執行緒 2.將執行緒號填寫到tid 3.返回函式呼叫
pthread_create(&tid, NULL, th_fun, (void *)&n);
printf("main PID = %d\n", getpid());
printf("main thread ID = %x\n",(unsigned int)pthread_self());
printf("main created thread = %x\n", (unsigned int)tid);
pthread_cancel(tid);
//同一程序的執行緒間,pthread_cancel向另一執行緒發終止訊號
//系統並不會馬上關閉被取消執行緒,只有在被取消執行緒下次系統呼叫時
//才會真正結束執行緒,執行後通過ps -eLf 和 ps -Lw pid可以檢視
//或者在要被取消的執行緒中呼叫thread_testcancel()檢測是否需要取消當前執行緒
while(1)
sleep(5);
return 0;
}