1. 程式人生 > >(四十一)執行緒——執行緒原語

(四十一)執行緒——執行緒原語

一、準備工作

  檢視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 *,這個指標的含義同樣由呼叫者自己定義

。start_routine返回時,這個執行緒就退出了,其它執行緒可以呼叫pthread_join得到start_routine的返回值,類似於父程序呼叫wait()得到子程序的退出狀態,稍後詳細介紹pthread_join。
  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;
}