1. 程式人生 > >25-執行緒終止詳解

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多執行緒中,呼叫以下任何函式都會設定一個取消點: 在這裡插入圖片描述

圖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. 總結

  1. 掌握執行緒終止,執行緒回收,執行緒分離
  2. 瞭解執行緒取消,理解取消點的概念