Linux 多線程編程基礎

Linux系統下的多線程遵循POSIX線程接口,稱爲pthread。編寫Linux下的多線程程序,需要使用頭文件pthread.h,連接時需要使用庫libpthread.a。與vxworks上任務的概念類似,都是調度的最小單元,都有共享的堆、棧、代碼區、全局變量等。

 

2.   創建線程

int  pthread_create(pthread_t  *  thread,

pthread_attr_t * attr,

void * (*start_routine)(void *),

void * arg)

thread:返回創建的線程的ID

attr:線程屬性,調度策略、優先級等都在這裏設置,如果爲NULL則表示用默認屬性

start_routine:線程入口函數,可以返回一個void*類型的返回值,該返回值可由pthread_join()捕獲

arg:傳給start_routine的參數, 可以爲NULL

返回值:成功返回0

 

3.   設置線程屬性

線程屬性通過attr進行設置。

設置與查詢attr結構的爲pthread_attr_get***()與pthread_attr_set***()兩個函數系列。

設置與查詢線程參數的爲pthread_get***()與pthread_set***()兩個函數系列。也可以在創建時通過pthrea_create傳入參數。

注:有些必須在線程創建時設置,如調度策略。

 

3.1.  調度策略

調度策略有三種:

SCHED_OTHER:非實時、正常

SCHED_RR:實時、輪詢法

SCHED_FIFO:實時、先入先出,與vxworks的調度機制一致

例程:

pthread_attr_t attr;

pthread_attr_init(&attr);

pthread_attr_setschedpolicy(&attr, SCHED_FIFO);//sched_policy

 

3.2.  優先級

線程優先級支持兩種設置方式,一種是創建時設置,另一種是創建後動態設置

例程:

pthread_attr_setschedparam(&attr, new_priority);

如果是靜態設置,則之後會用該屬性創建任務,如果是動態設置,則使用下列函數設置:

pthread_attr_setschedparam(&attr, &task->prv_priority);

pthread_attr_getschedparam(&attr, &schedparam);

schedparam.sched_priority = new_priority;

pthread_attr_setschedparam(&attr, &schedparam);

pthread_setschedparam(pthrid, sched_policy, &schedparam);

 

3.3.  脫離同步

Pthread_join()函數可以使主線程與子線程保持同步。如果設置了detachstate狀態,則pthread_join()會失效,線程會自動釋放所佔用的資源。線程的缺省狀態爲PHREAD_CREATE_JOINABLE狀態,線程運行起來後,一旦被設置爲PTHREAD_CREATE_DETACH狀態,則無法再恢復到joinable狀態。

例程:

pthread_attr_setinheritsched(&attr,PTHREAD_EXPLICIT_SCHED);、

 

3.4.  調度繼承

線程A創建了線程B,則線程B的調度策略是與線程A的調度策略與線程B的繼承策略有關的。如果線程B繼承策略爲PTHREAD_INHERIT_SCHED,則線程B的調度策略與線程A相同;如果線程B繼承策略爲PTHREAD_EXPLICIT_SCHE,則線程B的調度策略由attr決定。

pthread_attr_setdetachstate (&attr,PTHREAD_CREATE_DETACHED);

4.   線程取消

4.1.  線程取消定義

Pthread線程可以通過發送取消請求的方式終止一個線程的運行。

取消線程的操作主要應用於下列場景中:有一個線程在使用select監控網口,主控線程此時接到了用戶的通知,要放棄監聽,則主控線程會向監聽線程發送取消請求。

Linux的pthread線程接收到取消請求時,並不會立刻終止線程,而是要等到取消點時纔會結束任務。這樣我們可以爲取消點建立某些特殊的處理。Select是取消點,所以可以退出。

 

4.2.  取消點

Vxworks可以在任意位置殺死任務,這樣做導致任務被殺死後的位置不可控,所以vxworks中不會很少使用taskDeleteHook。

Pthread規定了取消點的概念。不論線程何時收到取消請求,都只能在取消點上才能取消線程。這就保證了風險的可控。

Pthread標準指定了以下幾個取消點:

Ø  pthread_testcancel

Ø  所有調度點,如pthread_cond_wait、sigwait、select、sleep等

根據POSIX標準,read()、write()等會引起阻塞的系統調用都是Cancelation-point,而其他pthread函數都不會引起Cancelation動作。但是pthread_cancel的手冊頁聲稱,由於LinuxThread庫與C庫結合得不好,因而目前C庫函數都不是Cancelation-point,因此可以在需要作爲Cancelation-point的系統調用前後調用pthread_testcancel(),從而達到POSIX標準所要求的目標,即如下代碼段:

pthread_testcancel();

retcode = read(fd, buffer, length);

pthread_testcancel();

這段代碼可以保證read附近有取消點,但是否有可能會卡在read中無法返回呢?

 

如果線程處於無限循環中,且循環體內沒有執行至取消點的必然路徑,則線程無法由外部其他線程的取消請求而終止。因此在這樣的循環體的必經路徑上應該加入pthread_testcancel()調用。

 

4.3.  線程取消函數

int pthread_cancel(pthread_t thread)

發送終止信號給thread線程,如果成功則返回0,否則爲非0值。發送成功並不意味着thread會終止。

int pthread_setcancelstate(int state, int *oldstate)

設置本線程對Cancel信號的反應,state有兩種值:PTHREAD_CANCEL_ENABLE(缺省)和PTHREAD_CANCEL_DISABLE,分別表示收到信號後設爲CANCLED狀態和忽略CANCEL信號繼續運行;old_state如果不爲NULL則存入原來的Cancel狀態以便恢復。

int pthread_setcanceltype(int type, int *oldtype)

設置本線程取消動作的執行時機,type由兩種取值:PTHREAD_CANCEL_DEFFERED和PTHREAD_CANCEL_ASYCHRONOUS,僅當Cancel狀態爲Enable時有效,分別表示收到信號後繼續運行至下一個取消點再退出和立即執行取消動作(退出);oldtype如果不爲NULL則存入原來的取消動作類型值。

void pthread_testcancel(void)

檢查本線程是否處於Canceld狀態,如果是,則進行取消動作,否則直接返回。

 

4.4.  例程

#include

#include

#include

 

pthread_key_t key;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_cond_t cond =   PTHREAD_COND_INITIALIZER;

unsigned long abc=0;

 

void* Test03(void *p)

{

    printf("Cancel point");

    return NULL;

}

 

void* Test01(void* ptr)

{

    pthread_cleanup_push(Test03, NULL); /* push */

    while(1)

    {

        abc++;

        pthread_testcancel();

    }

    pthread_cleanup_pop(0); /* pop */

    return NULL;

}

 

void* Test02(void* ptr)

{

    while(1)

    {

        sleep(2);

        printf("2222cond_wait:abc=0x%08x\n", abc);

    }

   

    return NULL;

}

 

int main(void)

{

    int tid1, tid2;

    int ret;

 

    printf("Start:\n");

    ret = pthread_create(&tid1, NULL, Test01, NULL);

    ret = pthread_create(&tid2, NULL, Test02, NULL);

 

    sleep(6);

    pthread_cancel(tid1);

    pthread_join(tid1, NULL);

    pthread_join(tid2, NULL);

   

    return 0;

}

 

結果:

Start:

2222cond_wait:abc=0x22c29a05

2222cond_wait:abc=0x47b49007

Cancel point2222cond_wait:abc=0x6c9de3ad

2222cond_wait:abc=0x6c9de3ad

2222cond_wait:abc=0x6c9de3ad

 

如果不加取消點pthread_testcancel(),線程1無法退出。

5.   線程終止方式

線程終止有兩種情況(不考慮進程),一種是線程主體函數return時,線程會自動終止,此時的退出是可預知的;另一種是其它線程向通以進程下的線程發送取消請求,線程會根據情況判斷是否終止,此時的終止是不可預知的。

Vxworks中的任務與此類似。任務的主體函數return時,任務會自動終止;其它任務調用taskDelete()可以殺死任意一個任務。TaskDelete()並不安全,因爲任務可能被殺死在任意一個時刻。

5.1.  線程終止時的清理

不論是可預見的線程終止還是異常終止,都會存在資源釋放的問題,在不考慮因運行出錯而退出的前提下,如何保證線程終止時能順利的釋放掉自己所佔用的資源,特別是鎖資源,就是一個必須考慮解決的問題。

最經常出現的情形是資源獨佔鎖的使用:線程爲了訪問臨界資源而爲其加上鎖,但在訪問過程中被外界取消,如果線程處於響應取消狀態,且採用異步方式響應,或者在打開獨佔鎖以前的運行路徑上存在取消點,則該臨界資源將永遠處於鎖定狀態得不到釋放。外界取消操作是不可預見的,因此的確需要一個機制來簡化用於資源釋放的編程。

在POSIX線程API中提供了一個pthread_cleanup_push()/pthread_cleanup_pop()函數對用於自動釋放資源,相當於是增加了一個析構函數。從pthread_cleanup_push()的調用點到pthread_cleanup_pop()之間的程序段中的終止動作(包括調用pthread_exit()和取消點終止)都將執行pthread_cleanup_push()所指定的清理函數。

API定義如下:

void pthread_cleanup_push(void (*routine) (void  *),  void *arg)

void pthread_cleanup_pop(int execute)

 

pthread_cleanup_push()/pthread_cleanup_pop()

採用先入後出的棧結構管理,void routine(void *arg)函數在調用pthread_cleanup_push()時壓入清理函數棧,多次對pthread_cleanup_push()的調用將在清理函數棧中形成一個函數鏈,在執行該函數鏈時按照壓棧的相反順序彈出。

execute參數表示執行到pthread_cleanup_pop()時是否在彈出清理函數的同時執行該函數,爲0表示不執行,非0爲執行;這個參數並不影響異常終止時清理函數的執行。

 

這兩個其實是宏,必須成對出現,否則會編譯不過。

編譯。在下面的例子裏,當線程在"do some work"中終止時,將主動調用pthread_mutex_unlock(mut),以完成解鎖動作。

pthread_cleanup_push(pthread_mutex_unlock, (void *) &mut);

pthread_mutex_lock(&mut);

/* do some work */

pthread_mutex_unlock(&mut);

pthread_cleanup_pop(0);

 

必須要注意的是,如果線程處於PTHREAD_CANCEL_ASYNCHRONOUS狀態,上述代碼段就有可能出錯,因爲CANCEL事件有可能在pthread_cleanup_push()和pthread_mutex_lock()之間發生,或者在pthread_mutex_unlock()和pthread_cleanup_pop()之間發生,從而導致清理函數unlock一個並沒有加鎖的mutex變量,造成錯誤。因此,在使用清理函數的時候,都應該暫時設置成PTHREAD_CANCEL_DEFERRED模式。爲此,pthread中還提供了一對不保證可移植的pthread_cleanup_push_defer_np()/pthread_cleanup_pop_defer_np()擴展函數,功能與以下代碼段相當:

{

int oldtype;

 pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &oldtype);

 pthread_cleanup_push(routine, arg);

 ...

 pthread_cleanup_pop(execute);

 pthread_setcanceltype(oldtype, NULL);

 }

 

5.2.  線程終止的同步及其返回值

一般情況下,進程中各個線程的運行都是相互獨立的,線程的終止並不會通知,也不會影響其他線程,終止的線程所佔用的資源也並不會隨着線程的終止而得到釋放。正如進程之間可以用wait()系統調用來同步終止並釋放資源一樣,線程之間也有類似機制,那就是pthread_join()函數。

void pthread_exit(void *retval)

int pthread_join(pthread_t th, void **thread_return)

int pthread_detach(pthread_t th)

 

pthread_join()的調用者將掛起並等待th線程終止,retval是pthread_exit()調用者線程(線程ID爲th)的返回值,如果thread_return不爲NULL,則*thread_return=retval。需要注意的是一個線程僅允許唯一的一個線程使用pthread_join()等待它的終止,並且被等待的線程應該處於可join狀態,即非DETACHED狀態。

如果進程中的某個線程執行了pthread_detach(th),則th線程將處於DETACHED狀態,這使得th線程在結束運行時自行釋放所佔用的內存資源,同時也無法由pthread_join()同步,pthread_detach()執行之後,對th請求pthread_join()將返回錯誤。

一個可join的線程所佔用的內存僅當有線程對其執行了pthread_join()後纔會釋放,因此爲了避免內存泄漏,所有線程的終止,要麼已設爲DETACHED,要麼就需要使用pthread_join()來回收。

 

5.3.  關於ptread_exite()return

理論上說,pthread_exit()和線程宿體函數退出的功能是相同的,函數結束時會在內部自動調用pthread_exit()來清理線程相關的資源。但實際上二者由於編譯器的處理有很大的不同。

在進程主函數(main())中調用pthread_exit(),只會使主函數所在的線程(可以說是進程的主線程)退出;而如果是return,編譯器將使其調用進程退出的代碼(如_exit()),從而導致進程及其所有線程結束運行。

其次,在線程宿主函數中主動調用return,如果return語句包含在pthread_cleanup_push()/pthread_cleanup_pop()對中,則不會引起清理函數的執行,反而會導致segment fault。

 

5.4.  判斷是否爲同一個線程

int pthread_equal(pthread_t thread1, pthread_t thread2)

判斷兩個線程描述符是否指向同一線程。在LinuxThreads中,線程ID相同的線程必然是同一個線程,因此,這個函數的實現僅僅判斷thread1和thread2是否相等。

 

5.5.  僅執行一次的操作

int pthread_once(pthread_once_t *once_control, void (*init_routine) (void))

本函數使用初值爲PTHREAD_ONCE_INIT的once_control變量保證init_routine()函數在本進程執行序列中僅執行一次。這個類似與線程的構造函數。

#include

#include

pthread_once_t  once=PTHREAD_ONCE_INIT;

void    once_run(void)

{

        printf("once_run in thread %d\n",pthread_self());

}

void * child1(void *arg)

{

        int tid=pthread_self();

        printf("thread %d enter\n",tid);

        pthread_once(&once,once_run);

        printf("thread %d returns\n",tid);

}

void * child2(void *arg)

{

        int tid=pthread_self();

        printf("thread %d enter\n",tid);

        pthread_once(&once,once_run);

        printf("thread %d returns\n",tid);

}

int main(void)

{

        int tid1,tid2;

        printf("hello\n");

        pthread_create(&tid1,NULL,child1,NULL);

        pthread_create(&tid2,NULL,child2,NULL);

        sleep(10);

        printf("main thread exit\n");

        return 0;

}

 

once_run()函數僅執行一次,且究竟在哪個線程中執行是不定的,儘管pthread_once(&once,once_run)出現在兩個線程中。

LinuxThreads使用互斥鎖和條件變量保證由pthread_once()指定的函數執行且僅執行一次,而once_control則表徵是否執行過。如果once_control的初值不是PTHREAD_ONCE_INIT(LinuxThreads定義爲0),pthread_once()的行爲就會不正常。在LinuxThreads中,實際"一次性函數"的執行狀態有三種:NEVER(0)、IN_PROGRESS(1)、DONE(2),如果once初值設爲1,則由於所有pthread_once()都必須等待其中一個激發"已執行一次"信號,因此所有pthread_once()都會陷入永久的等待中;如果設爲2,則表示該函數已執行過一次,從而所有pthread_once()都會立即返回0。

pthread_kill_other_threads_np()

void pthread_kill_other_threads_np(void)

這個函數是LinuxThreads針對本身無法實現的POSIX約定而做的擴展。POSIX要求當進程的某一個線程執行exec*系統調用在進程空間中加載另一個程序時,當前進程的所有線程都應終止。由於LinuxThreads的侷限性,該機制無法在exec中實現,因此要求線程執行exec前手工終止其他所有線程。pthread_kill_other_threads_np()的作用就是這個。

需要注意的是,pthread_kill_other_threads_np()並沒有通過pthread_cancel()來終止線程,而是直接向管理線程發"進程退出"信號,使所有其他線程都結束運行,而不經過Cancel動作,當然也不會執行退出回調函數。儘管LinuxThreads的實驗結果與文檔說明相同,但代碼實現中卻是用的__pthread_sig_cancel信號來kill線程,應該效果與執行pthread_cancel()是一樣的,其中原因目前還不清楚。

6.   TSD

6.1.  TSD概念

在單線程程序中,我們經常要用到"全局變量"以實現多個函數間共享數據。在多線程環境下,由於數據空間是共享的,因此全局變量也爲所有線程所共有。但有時應用程序設計中有必要提供線程私有的全局變量,僅在某個線程中有效,但卻可以跨多個函數訪問,比如程序可能需要每個線程維護一個鏈表,而使用相同的函數操作,最簡單的辦法就是使用同名而不同變量地址的線程相關數據結構。這樣的數據結構可以由Posix線程庫維護,稱爲線程私有數據(Thread-specific Data,或TSD)。

 

6.2.  創建與註銷

Posix定義了兩個API分別用來創建和註銷TSD:

int pthread_key_create(pthread_key_t *key, void (*destr_function) (void *))
 該函數從TSD池中分配一項,將其值賦給key供以後訪問使用。如果destr_function不爲空,在線程退出(pthread_exit())時將以key所關聯的數據爲參數調用destr_function(),以釋放分配的緩衝區。不論哪個線程調用pthread_key_create(),所創建的key都是所有線程可訪問的,但各個線程可根據自己的需要往key中填入不同的值,這就相當於提供了一個同名而不同值的全局變量。在LinuxThreads的實現中,TSD池用一個結構數組表示:
static struct pthread_key_struct pthread_keys[PTHREAD_KEYS_MAX] = { { 0, NULL } };
創建一個TSD就相當於將結構數組中的某一項設置爲"in_use",並將其索引返回給*key,然後設置destructor函數爲destr_function。註銷一個TSD採用如下API:
int pthread_key_delete(pthread_key_t key)
這個函數並不檢查當前是否有線程正使用該TSD,也不會調用清理函數(destr_function),而只是將TSD釋放以供下一次調用pthread_key_create()使用。在LinuxThreads中,它還會將與之相關的線程數據項設爲NULL(見"訪問")。 6.3.  訪問TSD的讀寫都通過專門的Posix Thread函數進行,其API定義如下:
int  pthread_setspecific(pthread_key_t  key,  const   void  *pointer)void * pthread_getspecific(pthread_key_t key)
寫入(pthread_setspecific())時,將pointer的值(不是所指的內容)與key相關聯,而相應的讀出函數則將與key相關聯的數據讀出來。數據類型都設爲void *,因此可以指向任何類型的數據。在LinuxThreads中,使用了一個位於線程描述結構(_pthread_descr_struct)中的二維void *指針數組來存放與key關聯的數據,數組大小由以下幾個宏來說明:
#define PTHREAD_KEY_2NDLEVEL_SIZE       32#define PTHREAD_KEY_1STLEVEL_SIZE   \((PTHREAD_KEYS_MAX + PTHREAD_KEY_2NDLEVEL_SIZE - 1)/ PTHREAD_KEY_2NDLEVEL_SIZE)    其中在/usr/include/bits/local_lim.h中定義了PTHREAD_KEYS_MAX爲1024,    因此一維數組大小爲32。而具體存放的位置由key值經過以下計算得到:idx1st = key / PTHREAD_KEY_2NDLEVEL_SIZEidx2nd = key % PTHREAD_KEY_2NDLEVEL_SIZE

也就是說,數據存放與一個32×32的稀疏矩陣中。同樣,訪問的時候也由key值經過類似計算得到數據所在位置索引,再取出其中內容返回。

 

6.4.  使用範例

/* fireaxe的例程 */

#include

#include

pthread_key_t   key;

void echomsg(int t)

{

        printf("destructor excuted in thread %d,param=%d\n",pthread_self(),t);

}

void * child1(void *arg)

{

        int tid=pthread_self();

        printf("thread %d enter\n",tid);

        pthread_setspecific(key,(void *)tid);

        sleep(2);

        printf("thread %d returns %d\n",tid,pthread_getspecific(key));

        sleep(5);

}

void * child2(void *arg)

{

        int tid=pthread_self();

        printf("thread %d enter\n",tid);

        pthread_setspecific(key,(void *)tid);

        sleep(1);

        printf("thread %d returns %d\n",tid,pthread_getspecific(key));

        sleep(5);

}

int main(void)

{

        int tid1,tid2;

        printf("hello\n");

        pthread_key_create(&key,echomsg);

        pthread_create(&tid1,NULL,child1,NULL);

        pthread_create(&tid2,NULL,child2,NULL);

        sleep(10);

        pthread_key_delete(key);

        printf("main thread exit\n");

        return 0;

}

 

給例程創建兩個線程分別設置同一個線程私有數據爲自己的線程ID,爲了檢驗其私有性,程序錯開了兩個線程私有數據的寫入和讀出的時間,從程序運行結果可以看出,兩個線程對TSD的修改互不干擾。同時,當線程退出時,清理函數會自動執行,參數爲tid。

7.   線程同步

7.1.  互斥鎖(互斥信號量)

Pthread的互斥鎖與vxworks的互斥信號量類似,都是用於互斥保護。

需要注意的是,linux有取消點的概念,即任務在運行時可以被取消。如果使用了互斥鎖,可能會造成爲解鎖就被取消。爲了解決這一問題,linux提供了可以在取消上加回調函數的功能:

pthread_cleanup_push()

pthread_cleanup_pop()

兩個必須成對出現如果線程運行到兩個函數之間被取消時,push註冊的函數會被調用。

例程:

如果沒有push與pop兩行,則tid1被殺死後會導致tid2無法獲取到互斥鎖。

/* fireaxe的例程 */

#include

#include

#include

 

pthread_key_t key;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_cond_t cond =   PTHREAD_COND_INITIALIZER;

 

void* Test01(void* ptr)

{

    pthread_cleanup_push(pthread_mutex_unlock, &mutex); /* push */

    while(1)

    {

        sleep(3);

        pthread_mutex_lock(&mutex);

        printf("1111mutex_lock\n");

        pthread_cond_wait(&cond, &mutex);

        printf("1111cond_wait\n");

        pthread_mutex_unlock(&mutex);

    }

    pthread_cleanup_pop(0); /* pop */

    return NULL;

}

 

void* Test02(void* ptr)

{

    while(1)

    {

        sleep(2);

        pthread_mutex_lock(&mutex);

        printf("2222mutex_lock\n");

        pthread_cond_wait(&cond, &mutex);

        printf("2222cond_wait\n");

        pthread_mutex_unlock(&mutex);

        sleep(2);

    }

   

    return NULL;

}

 

int main(void)

{

    int tid1, tid2;

    int ret;

 

    pthread_mutex_init(&mutex, NULL);

    pthread_mutex_init(&mutex, NULL);

    printf("Start:\n");

    ret = pthread_create(&tid1, NULL, Test01, NULL);

    ret = pthread_create(&tid2, NULL, Test02, NULL);

    printf("ret = 0x%x\n", ret);

 

    sleep(4);

    pthread_cancel(tid1);

   

    do

    {

        sleep(4);

        pthread_cond_signal(&cond);

    } while(1);

    printf("end.\n");

   

    return 0;

   

}

 

7.2.  條件變量(同步信號量)

Pthread的條件變量與vxworks中同步信號量類似,都是用於任務同步。區別是條件變量的操作不是原子操作,需要藉助互斥鎖保證其原子性。7.3.  信號燈(計數信號量)

Pthread的信號燈與vxworks中計數信號量信號量類似,都是用於表示資源是否可用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章