25-執行緒終止詳解
1. 執行緒終止
執行緒的終止包括主動終止和被動終止兩大類。
主動終止: 執行緒主函式執行return正常返回,返回值是執行緒的退出碼。
執行緒主函式執行pthread_exit函式退出,其引數是一個傳出引數,儲存執行緒退出碼。
被動終止: 在同一程序中其他執行緒呼叫pthread_cancel函式。
任意執行緒呼叫了exit、_Exit、_exit 導致整個程序終止,又或者主執行緒在main函式中執行return語句都會導致程序中的所有執行緒立即終止。
pthread_exit函式的作用是將單個執行緒退出。
void pthread_exit(void *retval);
引數retval:是一個void *型別的傳出引數,儲存著執行緒退出狀態,如果不關心執行緒的退出狀態可設定為NULL(傳出引數)。
2. 執行緒終止實驗
執行緒退出實驗程式碼:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
void *tfn(void *arg){
int i = (int)arg;
sleep(i);
//讓第3個執行緒退出
if (i == 2) {
//exit(1); 使用exit函式會導致整個程式退出,禁止使用
//return NULL 會使當前函式結束並返回
pthread_exit(NULL); //將單個執行緒退出
}
printf("I'm %dth pthread tid = %lu\n", i+1, pthread_self());
return NULL;
}
int main(void) {
pthread_t tid;
int i, ret;
//建立了5個執行緒
for (i = 0; i < 5; i++) {
ret = pthread_create(&tid, NULL, tfn, (void *)i);
if (ret != 0) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(1);
}
}
printf("I'm main pthread tid = %lu\n", pthread_self());
//只讓主執行緒退出
pthread_exit((void *)0);
//如果直接return的話,會將整個程序結束
//return 0;
}
程式執行結果:
從圖中可以看到程式建立了5個執行緒,並讓第3個執行緒呼叫pthread_exit提前終止了。如果主執行緒呼叫了pthread_exit函式,而非呼叫exit或執行return語句,那麼其他執行緒將會正常執行,並不會退出。
另注意,pthread_exit或者return返回的指標所指向的記憶體單元必須是全域性的或者是用malloc分配的,不能線上程主函式的棧上分配,因為當其它執行緒得到這個返回指標時,執行緒主函式已經退出了。
3. 回收執行緒資源
pthread_join會阻塞等待
當前執行緒,直到指定的執行緒退出或執行pthread_exit函式,然後將執行緒回收,對應程序中 waitpid() 函式(pthread_join會阻塞,waitpid可以非阻塞)。
int pthread_join(pthread_t thread , void **retval);
引數說明: thread:等待回收的執行緒id retval:儲存執行緒結束狀態資訊,如果不關心執行緒退出狀態可設定為NULL
注意:如果執行緒通過return或exit結束時,在程序結束前,執行緒的資源並沒有完全釋放,這將會產生殭屍執行緒(類似於之前講過的殭屍程序,可參考:19-孤兒程序與殭屍程序),所以需要通過pthread_join函式回收執行緒。
另外,呼叫一次pthread_join函式只回收一個執行緒,如果要回收多個執行緒需多次呼叫pthread_join函式。
回收執行緒實驗:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
//執行緒資訊
typedef struct{
char name[30];
int age;
} student;
//執行緒主控函式
void *tfn(void *arg) {
student *s1 = NULL;
s1 = malloc(sizeof(student));
strcpy(s1->name , "zhangsan");
s1->age = 20;
pthread_exit((void *)s1);
return NULL;
}
int main(void){
pthread_t tid;
student *retval;
pthread_create(&tid, NULL, tfn, NULL);
//呼叫pthread_join可以獲取執行緒退出時的狀態資訊
pthread_join(tid, (void **)&retval);
printf("name = %s, age = %d \n", retval->name, retval->age);
if(retval != NULL){
free(retval);
retval = NULL;
}
//printf(“a = %d , b = %d\n”, (student)retval->name , (student)retval->age);
return 0;
}
程式執行結果:
4. 執行緒分離
pthread_detach函式用於實現執行緒分離,分離的好處就是分離出來的執行緒執行結束,它將自動銷燬,清理,不需要手動回收執行緒。
int pthread_detach(pthread_t thread);
引數thread:指定要分離的執行緒id
分離(detach)狀態:執行緒主動與主控執行緒斷開連線關係
執行緒一旦處於分離狀態,則不能通過pthread_join函式來獲取該執行緒的狀態,因為執行緒分離後,其狀態是不確定的。因此,如果不關心執行緒退出狀態,希望執行緒退出時自動清理並銷燬的話,使用pthread_detach是個不錯的選擇。
如果其他執行緒呼叫了exit或主執行緒執行return語句時,即便該執行緒已經處於分離狀態,程序的所有執行緒都會退出。換句話說,pthread_detach函式只能控制執行緒終止之後所發生的事,而非何時終止執行緒
。
執行緒分離實驗:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
//執行緒主控函式
void *tfn(void *arg) {
int n = 5;
while (n--) {
printf("pthread tfn n = %d\n", n);
sleep(1);
}
return (void *)7;
}
int main(void) {
pthread_t tid;
int ret = pthread_create(&tid, NULL, tfn, NULL);
//執行緒分離
pthread_detach(tid);
//一般來說執行緒已經處於分離態,就不能通過pthread_join來回收執行緒
int retvar = pthread_join(tid, (void **)&ret);
//pthread_join非0表示失敗
if (retvar != 0) {
fprintf(stderr, "pthread_join error %s\n", strerror(retvar));
} else {
printf("pthread exit with %d\n", (int)ret);
}
sleep(6);
return 0;
}
程式執行結果:
當設定執行緒為分離狀態後,呼叫pthread_join函式獲取執行緒退出狀態時出錯了,這是因為處於detach狀態的執行緒不會保留狀態,所以再呼叫pthread_join函式獲取狀態就會返回EINVAL。
EINVAL錯誤的意思是pthread_join函式指定的執行緒處於分離狀態。
一旦執行緒處於detach狀態,就不能再呼叫pthread_join
5. 執行緒取消
一個執行緒可以呼叫pthread_cancel取消(終止)其他執行緒,pthread_cancel並不關心執行緒什麼時候終止,它僅僅提出請求,pthread_cancel函式有點類似於程序中 kill() 函式(pthread_cancel只能取消同一個程序裡的其他執行緒)。
int pthread_cancel(pthread_t thread);
引數:指定要取消的執行緒id
預設情況下,pthread_cancel函式指定的執行緒可以繼續執行,直到執行緒到達某個取消點,如果設定了取消點,那麼執行相應的動作,即殺死執行緒。
什麼是取消點?
取消點是檢查執行緒是否被取消,並按請求進行處理動作的一個位置(APUE的說法)。
簡單來說,可以粗略的把取消點看做是一個系統呼叫
,在POSIX.1多執行緒中,呼叫以下任何函式都會設定一個取消點:
6. 執行緒取消實驗1
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
//執行緒1主控函式
void *tfn1(void *arg) {
printf("thread 1 returning\n");
return (void *)111;
}
//執行緒2主控函式
void *tfn2(void *arg) {
printf("thread 2 exiting\n");
pthread_exit((void *)222);
}
//執行緒3主控函式
void *tfn3(void *arg) {
while (1) {
puts("thread 3 will going to die , 3 seconds after");
sleep(1); //根據圖1所示,sleep呼叫本身就是一個取消點,預設處理動作就是殺死執行緒
}
}
int main(void) {
pthread_t tid;
void *tret = NULL;
//建立第一個執行緒
pthread_create(&tid, NULL, tfn1, NULL);
pthread_join(tid, &tret);
printf("thread 1 exit code = %d\n\n", (int)tret);
//建立第二個執行緒
pthread_create(&tid, NULL, tfn2, NULL);
pthread_join(tid, &tret);
printf("thread 2 exit code = %d\n\n", (int)tret);
//建立第三個執行緒
pthread_create(&tid, NULL, tfn3, NULL);
//為了檢視前面2個執行緒的狀態,睡眠3秒
sleep(3);
//把第三個執行緒設定為取消
pthread_cancel(tid);
pthread_join(tid, &tret);
printf("\nthread 3 is die , exit code = %d\n", (int)tret);
return 0;
}
程式執行結果:
執行緒3在等待了3秒後,最後退出時返回的狀態是-1,說明執行緒3是異常退出的,被指定為取消執行緒。
現在我們對執行緒3的執行緒主函式做以下修改:
//執行緒3主控函式
void *tfn3(void *arg) {
while (1) {
//puts("thread 3 will going to die , 3 seconds after");
//sleep(1);
//此時執行緒3沒有取消點
}
}
程式執行結果:
執行緒3不設定取消點後,即便執行緒3被設定為取消也不會結束,此時執行緒3正在while迴圈嗨皮的空轉,而主執行緒還一直傻傻的阻塞等待執行緒3終止呢。
這說明了pthread_cancel函式僅僅提出取消的請求,簡而言之,執行緒的取消並不是實時的,而是有一定的延時,需要等待執行緒到達某個取消點(檢查點)才會執行處理動作(終止程序)。
7. pthread_testcancel函式
通過前面可知,即便執行緒被設定為取消,但沒有到達取消點,執行緒依然不會終止,那有沒有一種方法讓被設定為取消的執行緒,但沒有到達取消點的執行緒退出呢?答案是有的,pthread_testcancel函式就是用於檢查執行緒是否處於cancel(取消點)狀態
,如果執行緒被設定為取消,那麼將會執行處理動作殺死執行緒。
#include <pthread.h>
void pthread_testcancel(void)
還是對執行緒3的執行緒主函式做以下修改:
//執行緒3主控函式
void *tfn3(void *arg) {
while (1) {
//puts("thread 3 will going to die , 3 seconds after");
//sleep(1);
/*檢查執行緒是否處於取消狀態,如果是,則終止執行緒*/
pthread_testcancel();
}
}
程式執行結果:
呼叫pthread_cancel函式設定執行緒3為取消狀態後,即便執行緒3沒有到達取消點,依然能終止執行緒3,有同學可能會疑惑,為啥執行緒3退出的狀態是-1?
其實被取消的執行緒的退出值定義在Linux的pthread庫中,常數PTHREAD_CANCELED的值是-1,可以在標頭檔案pthread.h中找到它的定義:#define PTHREAD_CANCELED ((void *) -1)
pthread_cancel函式是一個很詭異的函式,坑多,不推薦大家使用。
8. 總結
- 掌握執行緒終止,執行緒回收,執行緒分離
- 瞭解執行緒取消,理解取消點的概念