1. 程式人生 > 其它 >Linux 系統程式設計 學習:9-執行緒:執行緒的建立、回收與取消

Linux 系統程式設計 學習:9-執行緒:執行緒的建立、回收與取消

概念

基礎概念:

  • 執行緒是cpu或作業系統排程的基本單位。執行緒大部分的資源是共享的,僅僅申請了自己的棧、空間。
  • 執行緒是程序內部的一個執行分支,執行緒量級很小。
  • 在程式中建立執行緒,可以提高效率,程序內執行緒越多,爭奪到CPU的概率就越大,執行程式碼的概率就越大(有一個度)。
  • 執行緒可以解決很多問題,而不會像程序一樣有那麼多的開銷。
  • 線上程中需要注意同步的問題。一個執行緒的bug很可能會引起該程序的崩潰。

執行緒與程序的記憶體分佈不同:

  • 每個程序在建立的時候都申請了新的記憶體空間以儲存程式碼段\資料段\BSS段\堆\棧空間,並且這些的空間的初始化值是父程序空間的,父子程序在建立後不能互訪資源。
  • 而每個新建立的執行緒則僅僅申請了自己的棧、空間;與同進程的其他執行緒共享該程序的其他資料空間包括程式碼段\資料段\BSS段\堆以及開啟的庫、mmap對映的檔案與共享的空間,使得同進程下的執行緒共享資料十分的方便,只需藉助這些共享區域即可,但也有問題即是同步問題。

執行緒開發基本步驟

在接下來的開發中,我們會介紹有關的函式;

所有執行緒是在<pthread.h>中,且編譯時需要連結pthread庫;下面不再說明。
同時,如果沒有特殊說明,所有的函式在失敗時都返回錯誤號(error number)。

執行緒的建立、回收與取消

建立執行緒

c
intpthread_create
(pthread_t *thread, constpthread_attr_t *attr, void *(*start_routine) (void *), void *arg)
;

描述:建立一個執行緒,執行緒建立成功以後,開始執行指定的函式。

預設情況下,一個執行緒所使用的記憶體資源在應用pthread_join呼叫之前不會被重新分配,所以對於每個執行緒必須呼叫一次pthread_join函式(分離執行緒除外)。

引數解析:

thread:存放執行緒ID號的物件

attr:建立執行緒時設定的有關屬性,可為空(這裡先略過,我們會在後面專門講到)

start_routine:執行緒執行的函式入口

arg:執行函式時附帶的引數,可為空

返回值:成功返回0,從thread可以獲取到執行緒ID;失敗返回錯誤號。

回收執行緒

c
intpthread_join(pthread_t thread, void **retval);

描述:阻塞等待一個執行緒,並回收其資源。

預設情況下,新建立的執行緒是joinable的,執行緒退出後,需對其進行pthread_join操作。

如果不關心執行緒的返回值,我們可以告訴系統,當執行緒退出時,自動釋放執行緒資源(後面我們會講到)

引數解析:

thread:指定等待的執行緒號

retval:非空時,獲取由pthread_exit(void *retval);函式傳過來的結果。

返回值:成功返回0,失敗返回error number。

提出一個終止執行緒的請求

c
intpthread_cancel(pthread_t thread);

描述:一般情況下,執行緒在其主體函式退出的時候會自動終止,但同時也可以因為接收到另一個執行緒發來的終止(取消)請求而強制終止。

同一程序的執行緒間,pthread_cancel向另一執行緒發終止訊號。如何處理Cancel訊號則由目標執行緒自己決定:忽略、立即終止、繼續執行至Cancelation-point(取消點),由不同的Cancelation狀態(pthread_setcancelstate函式設定狀態)決定。

被取消執行緒可以呼叫pthread_testcancel,讓核心去檢測是否需要取消當前執行緒。被取消的執行緒退出時總是返回-1(常數值PTHREAD_CANCELED)。

如果在取消功能處處於禁用狀態下呼叫pthread_testcancel(),則該函式不起作用。 請務必僅線上程取消執行緒操作安全的序列中插入pthread_testcancel()。除通過pthread_testcancel()呼叫以程式設計方式建立的取消點意外,pthread標準還指定了幾個取消點。測試退出點,就是測試cancel訊號

返回值: 傳送成功返回0(不意味著thread會終止);失敗返回錯誤號。

一個取消執行緒的例程
c
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>

void *thread_fun(void *arg){
    int i=1;
    printf("thread start \n");
    while(1)
    {
        // pthread_testcancel(); 如果沒有這句話,那麼執行緒不會結束。
        i++;
    }
    return (void *)0;
}

intmain(int argc, char* argv){
    void *ret = NULL;
    int iret = 0;
    pthread_t tid;
    pthread_create(&tid, NULL, thread_fun, NULL);
    sleep(1);
    pthread_cancel(tid);//取消執行緒
    pthread_join(tid, &ret);
    printf("thread 3 exit code %d\n", (int)ret);
    return 0;
}

執行緒取消

執行緒在收到取消請求(pthread_cancel)後會繼續執行,直到到達某個取消點(CancellationPoint)。

取消點:執行緒檢查是否被取消並按照請求進行動作的一個位置。

pthreads標準指定了幾個取消點,其中包括:
(1)通過pthread_testcancel呼叫以程式設計方式建立執行緒取消點。
(2)執行緒等待pthread_cond_waitpthread_cond_timewait()中的特定條件。 (錯誤的程式設計可能會在取消時導致死鎖)
(3)被sigwait()阻塞的函式
(4)一些標準的庫呼叫。通常,這些呼叫包括執行緒可基於阻塞的函式。

設定一個執行緒能否被取消

c
intpthread_setcancelstate(int state, int *oldstate)

描述:設定一個執行緒能否被取消

引數解析:

state: 狀態

  • PTHREAD_CANCEL_ENABLE(預設動作,收到訊號後設為CANCLED狀態)
  • PTHREAD_CANCEL_DISABLE(忽略CANCEL訊號繼續執行)

old_state: 舊狀態容器,如果不為 NULL則存入原來的Cancel狀態。

設定本執行緒取消動作的執行時機

c
intpthread_setcanceltype(int type, int *oldtype)

描述:設定本執行緒取消動作的執行時機(僅當Cancel狀態為Enable時有效)

引數解析:

type : 取消型別

  • PTHREAD_CANCEL_DEFFERED (預設,收到訊號後繼續執行至下一個取消點再退出)
  • PTHREAD_CANCEL_ASYCHRONOUS, (立即執行取消動作——退出)

oldtype : 舊狀態容器,如果不為NULL則存入運來的取消動作型別值。

手動建立取消點

c
voidpthread_testcancel(void)

描述:手動建立一個取消點,但執行緒設定了PTHREAD_CANCEL_ENABLEPTHREAD_CANCEL_DEFFERED屬性,且已經有執行緒傳送了取消本執行緒的請求時,退出;否則直接返回。

注意:由於此函式線上程內執行,執行的位置就是執行緒退出的位置,所以在執行此函式以前,執行緒內部的相關資源申請一定要釋放掉,很容易造成記憶體洩露

總結:
1)執行緒可以呼叫pthread_setcancelstate()設定被取消的,或者不能被取消的
2)執行緒的取消的本質是處理取消訊號
3)取消執行緒可以馬上進行的,也可以在取消點才取消 (pthread_setcanceltype()

若是在整個程式退出時,要終止各個執行緒,應該在成功傳送 CANCEL 指令後,使用 pthread_join 函式, 等待指定的執行緒已經完全退出以後, 再繼續執行; 否則,很容易產生 “段錯誤”。

執行緒遺囑

遺囑機制一般用於釋放一些資源,比如釋放鎖,以免其它的執行緒永遠 也不能獲得鎖,而造成死鎖。

c
voidpthread_cleanup_push(void (*routine)(void *), void *arg);
voidpthread_cleanup_pop(int execute);

登記執行壓棧清理函式

c
voidpthread_cleanup_push(void (*routine)(void *), void *arg);

描述:登記執行壓棧清理函式的操作。

引數解析:

routine:清理函式

arg:清理函式的有關引數

當以下描述的情況發生時自動呼叫的函式:

  • 執行緒呼叫pthread_exit()函式,而不是直接return.
  • 響應取消請求時,也就是有其它的執行緒對該執行緒呼叫pthread_cancel()函式而到達取消條件時。
  • 本執行緒呼叫pthread_cleanup_pop()函式,並且其引數非0。

從棧中刪除清理函式的操作

c
voidpthread_cleanup_pop(int execute);

描述:從棧中刪除清理函式的操作。

引數解析:

execute:標誌位,當其非0時,執行pthread_cleanup_push中登記好的函式;否則,將其出棧,不執行。

例程

2條執行緒使用互斥鎖(後面會講到)搶佔一個資源,當佔有鎖的其中一個執行緒被意外中斷時之前寫好了釋放鎖的遺囑,另外一條能夠正常拿到鎖。

c
#include<stdio.h>
#include<unistd.h>

#include<stdlib.h>
#include<pthread.h>
pthread_mutex_t mutex_x=PTHREAD_MUTEX_INITIALIZER;

voidclean_handler(void *arg){
    pthread_mutex_unlock((pthread_mutex_t *)arg); // 防止死鎖,所以在這裡新增解鎖操作
    printf("unlocked from clean_handler push by thread_fun1\n");
}

void *thread_fun1(void *arg){
    int i=1;
    printf("thread1 start \n");
    pthread_cleanup_push(clean_handler, &mutex_x);//提前登記執行緒被取消後需要處理的事情
    pthread_mutex_lock(&mutex_x);
    printf("thread_fun1 locked.\n");
    while(1)
    {
        pthread_testcancel(); //如果沒有這句話,那麼執行緒不會結束。
        i++;
    }
    pthread_mutex_unlock(&mutex_x);
    printf("thread_fun1 unlocked\n");
    pthread_cleanup_pop(0);
    return (void *)0;
}


void *thread_fun2(void *arg){
    int i=1;
    printf("thread2 start \n");
    for (i = 0; i < 6; ++i)
    {
        pthread_mutex_lock(&mutex_x);
        printf("thread_fun2 locked.\n");
        sleep(1);
        pthread_mutex_unlock(&mutex_x);
        printf("thread_fun2 unlocked\n");
    }
    printf("thread2 end \n");
    return (void *)0;
}

intmain(int argc, char* argv){
    void *ret = NULL;
    int iret = 0;
    pthread_t tid1;
    pthread_t tid2;
    pthread_create(&tid1, NULL, thread_fun1, NULL);
    pthread_create(&tid2, NULL, thread_fun2, NULL);
    sleep(2);
    pthread_cancel(tid1);//取消執行緒
    pthread_join(tid1, &ret);

    iret =(int)( (int*)ret);
    printf("thread 3 exit code %d\n", iret);
    while(1);
    return 0;
}

概念

基礎概念:

  • 執行緒是cpu或作業系統排程的基本單位。執行緒大部分的資源是共享的,僅僅申請了自己的棧、空間。
  • 執行緒是程序內部的一個執行分支,執行緒量級很小。
  • 在程式中建立執行緒,可以提高效率,程序內執行緒越多,爭奪到CPU的概率就越大,執行程式碼的概率就越大(有一個度)。
  • 執行緒可以解決很多問題,而不會像程序一樣有那麼多的開銷。
  • 線上程中需要注意同步的問題。一個執行緒的bug很可能會引起該程序的崩潰。

執行緒與程序的記憶體分佈不同:

  • 每個程序在建立的時候都申請了新的記憶體空間以儲存程式碼段\資料段\BSS段\堆\棧空間,並且這些的空間的初始化值是父程序空間的,父子程序在建立後不能互訪資源。
  • 而每個新建立的執行緒則僅僅申請了自己的棧、空間;與同進程的其他執行緒共享該程序的其他資料空間包括程式碼段\資料段\BSS段\堆以及開啟的庫、mmap對映的檔案與共享的空間,使得同進程下的執行緒共享資料十分的方便,只需藉助這些共享區域即可,但也有問題即是同步問題。

執行緒開發基本步驟

在接下來的開發中,我們會介紹有關的函式;

所有執行緒是在<pthread.h>中,且編譯時需要連結pthread庫;下面不再說明。
同時,如果沒有特殊說明,所有的函式在失敗時都返回錯誤號(error number)。

執行緒的建立、回收與取消

建立執行緒

c
intpthread_create(pthread_t *thread, constpthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);

描述:建立一個執行緒,執行緒建立成功以後,開始執行指定的函式。

預設情況下,一個執行緒所使用的記憶體資源在應用pthread_join呼叫之前不會被重新分配,所以對於每個執行緒必須呼叫一次pthread_join函式(分離執行緒除外)。

引數解析:

thread:存放執行緒ID號的物件

attr:建立執行緒時設定的有關屬性,可為空(這裡先略過,我們會在後面專門講到)

start_routine:執行緒執行的函式入口

arg:執行函式時附帶的引數,可為空

返回值:成功返回0,從thread可以獲取到執行緒ID;失敗返回錯誤號。

回收執行緒

c
intpthread_join(pthread_t thread, void **retval);

描述:阻塞等待一個執行緒,並回收其資源。

預設情況下,新建立的執行緒是joinable的,執行緒退出後,需對其進行pthread_join操作。

如果不關心執行緒的返回值,我們可以告訴系統,當執行緒退出時,自動釋放執行緒資源(後面我們會講到)

引數解析:

thread:指定等待的執行緒號

retval:非空時,獲取由pthread_exit(void *retval);函式傳過來的結果。

返回值:成功返回0,失敗返回error number。

提出一個終止執行緒的請求

c
intpthread_cancel(pthread_t thread);

描述:一般情況下,執行緒在其主體函式退出的時候會自動終止,但同時也可以因為接收到另一個執行緒發來的終止(取消)請求而強制終止。

同一程序的執行緒間,pthread_cancel向另一執行緒發終止訊號。如何處理Cancel訊號則由目標執行緒自己決定:忽略、立即終止、繼續執行至Cancelation-point(取消點),由不同的Cancelation狀態(pthread_setcancelstate函式設定狀態)決定。

被取消執行緒可以呼叫pthread_testcancel,讓核心去檢測是否需要取消當前執行緒。被取消的執行緒退出時總是返回-1(常數值PTHREAD_CANCELED)。

如果在取消功能處處於禁用狀態下呼叫pthread_testcancel(),則該函式不起作用。 請務必僅線上程取消執行緒操作安全的序列中插入pthread_testcancel()。除通過pthread_testcancel()呼叫以程式設計方式建立的取消點意外,pthread標準還指定了幾個取消點。測試退出點,就是測試cancel訊號

返回值: 傳送成功返回0(不意味著thread會終止);失敗返回錯誤號。

一個取消執行緒的例程
c
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>

void *thread_fun(void *arg){
    int i=1;
    printf("thread start \n");
    while(1)
    {
        // pthread_testcancel(); 如果沒有這句話,那麼執行緒不會結束。
        i++;
    }
    return (void *)0;
}

intmain(int argc, char* argv){
    void *ret = NULL;
    int iret = 0;
    pthread_t tid;
    pthread_create(&tid, NULL, thread_fun, NULL);
    sleep(1);
    pthread_cancel(tid);//取消執行緒
    pthread_join(tid, &ret);
    printf("thread 3 exit code %d\n", (int)ret);
    return 0;
}

執行緒取消

執行緒在收到取消請求(pthread_cancel)後會繼續執行,直到到達某個取消點(CancellationPoint)。

取消點:執行緒檢查是否被取消並按照請求進行動作的一個位置。

pthreads標準指定了幾個取消點,其中包括:
(1)通過pthread_testcancel呼叫以程式設計方式建立執行緒取消點。
(2)執行緒等待pthread_cond_waitpthread_cond_timewait()中的特定條件。 (錯誤的程式設計可能會在取消時導致死鎖)
(3)被sigwait()阻塞的函式
(4)一些標準的庫呼叫。通常,這些呼叫包括執行緒可基於阻塞的函式。

設定一個執行緒能否被取消

c
intpthread_setcancelstate(int state, int *oldstate)

描述:設定一個執行緒能否被取消

引數解析:

state: 狀態

  • PTHREAD_CANCEL_ENABLE(預設動作,收到訊號後設為CANCLED狀態)
  • PTHREAD_CANCEL_DISABLE(忽略CANCEL訊號繼續執行)

old_state: 舊狀態容器,如果不為 NULL則存入原來的Cancel狀態。

設定本執行緒取消動作的執行時機

c
intpthread_setcanceltype(int type, int *oldtype)

描述:設定本執行緒取消動作的執行時機(僅當Cancel狀態為Enable時有效)

引數解析:

type : 取消型別

  • PTHREAD_CANCEL_DEFFERED (預設,收到訊號後繼續執行至下一個取消點再退出)
  • PTHREAD_CANCEL_ASYCHRONOUS, (立即執行取消動作——退出)

oldtype : 舊狀態容器,如果不為NULL則存入運來的取消動作型別值。

手動建立取消點

c
voidpthread_testcancel(void)

描述:手動建立一個取消點,但執行緒設定了PTHREAD_CANCEL_ENABLEPTHREAD_CANCEL_DEFFERED屬性,且已經有執行緒傳送了取消本執行緒的請求時,退出;否則直接返回。

注意:由於此函式線上程內執行,執行的位置就是執行緒退出的位置,所以在執行此函式以前,執行緒內部的相關資源申請一定要釋放掉,很容易造成記憶體洩露

總結:
1)執行緒可以呼叫pthread_setcancelstate()設定被取消的,或者不能被取消的
2)執行緒的取消的本質是處理取消訊號
3)取消執行緒可以馬上進行的,也可以在取消點才取消 (pthread_setcanceltype()

若是在整個程式退出時,要終止各個執行緒,應該在成功傳送 CANCEL 指令後,使用 pthread_join 函式, 等待指定的執行緒已經完全退出以後, 再繼續執行; 否則,很容易產生 “段錯誤”。

執行緒遺囑

遺囑機制一般用於釋放一些資源,比如釋放鎖,以免其它的執行緒永遠 也不能獲得鎖,而造成死鎖。

c
voidpthread_cleanup_push(void (*routine)(void *), void *arg);
voidpthread_cleanup_pop(int execute);

登記執行壓棧清理函式

c
voidpthread_cleanup_push(void (*routine)(void *), void *arg);

描述:登記執行壓棧清理函式的操作。

引數解析:

routine:清理函式

arg:清理函式的有關引數

當以下描述的情況發生時自動呼叫的函式:

  • 執行緒呼叫pthread_exit()函式,而不是直接return.
  • 響應取消請求時,也就是有其它的執行緒對該執行緒呼叫pthread_cancel()函式而到達取消條件時。
  • 本執行緒呼叫pthread_cleanup_pop()函式,並且其引數非0。

從棧中刪除清理函式的操作

c
voidpthread_cleanup_pop(int execute);

描述:從棧中刪除清理函式的操作。

引數解析:

execute:標誌位,當其非0時,執行pthread_cleanup_push中登記好的函式;否則,將其出棧,不執行。

例程

2條執行緒使用互斥鎖(後面會講到)搶佔一個資源,當佔有鎖的其中一個執行緒被意外中斷時之前寫好了釋放鎖的遺囑,另外一條能夠正常拿到鎖。

c
#include<stdio.h>
#include<unistd.h>

#include<stdlib.h>
#include<pthread.h>
pthread_mutex_t mutex_x=PTHREAD_MUTEX_INITIALIZER;

voidclean_handler(void *arg){
    pthread_mutex_unlock((pthread_mutex_t *)arg); // 防止死鎖,所以在這裡新增解鎖操作
    printf("unlocked from clean_handler push by thread_fun1\n");
}

void *thread_fun1(void *arg){
    int i=1;
    printf("thread1 start \n");
    pthread_cleanup_push(clean_handler, &mutex_x);//提前登記執行緒被取消後需要處理的事情
    pthread_mutex_lock(&mutex_x);
    printf("thread_fun1 locked.\n");
    while(1)
    {
        pthread_testcancel(); //如果沒有這句話,那麼執行緒不會結束。
        i++;
    }
    pthread_mutex_unlock(&mutex_x);
    printf("thread_fun1 unlocked\n");
    pthread_cleanup_pop(0);
    return (void *)0;
}


void *thread_fun2(void *arg){
    int i=1;
    printf("thread2 start \n");
    for (i = 0; i < 6; ++i)
    {
        pthread_mutex_lock(&mutex_x);
        printf("thread_fun2 locked.\n");
        sleep(1);
        pthread_mutex_unlock(&mutex_x);
        printf("thread_fun2 unlocked\n");
    }
    printf("thread2 end \n");
    return (void *)0;
}

intmain(int argc, char* argv){
    void *ret = NULL;
    int iret = 0;
    pthread_t tid1;
    pthread_t tid2;
    pthread_create(&tid1, NULL, thread_fun1, NULL);
    pthread_create(&tid2, NULL, thread_fun2, NULL);
    sleep(2);
    pthread_cancel(tid1);//取消執行緒
    pthread_join(tid1, &ret);

    iret =(int)( (int*)ret);
    printf("thread 3 exit code %d\n", iret);
    while(1);
    return 0;
}