在Linux中使用線程

我並不假定你會使用Linux的線程,因此在這裏就簡單的介紹一下。若是你以前有過多線程方面的編程經驗,徹底能夠忽略本文的內容,由於它很是的初級。
程序員

首先說明一下,在Linux編寫多線程程序須要包含頭文件pthread.h。也就是說你在任何採用多線程設計的程序中都會看到相似這樣的代碼:
#include <pthread.h>  

固然,進包含一個頭文件是不能搞定線程的,還須要鏈接libpthread.so這個庫,所以在程序鏈接階段應該有相似這樣的指令:算法

gcc program.o -o program -lpthread

1. 第一個例子

 
在Linux下建立的線程的API接口是pthread_create(),它的完整定義是:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*) void *arg);  
當你的程序調用了這個接口以後,就會產生一個線程,而這個線程的入口函數就是start_routine()。若是線程建立成功,這個接口會返回0。
start_routine()函數有一個參數,這個參數就是pthread_create的最後一個參數arg。這種設計能夠在線程建立以前就幫它準備好一些專有數據,最典型的用法就是使用C++編程時的this指針。start_routine()有一個返回值,這個返回值能夠經過pthread_join()接口得到。
pthread_create()接口的第一個參數是一個返回參數。當一個新的線程調用成功以後,就會經過這個參數將線程的句柄返回給調用者,以便對這個線程進行管理。
pthread_create()接口的第二個參數用於設置線程的屬性。這個參數是可選的,當不須要修改線程的默認屬性時,給它傳遞NULL就行。具體線程有那些屬性,咱們後面再作介紹。
好,那麼咱們就利用這些接口,來完成在Linux上的第一個多線程程序,見代碼1所示:
#include <stdio.h>
#include <pthread.h>
void* thread( void *arg )
{
    printf( "This is a thread and arg = %d.\n", *(int*)arg);
    *(int*)arg = 0;
    return arg;
}
int main( int argc, char *argv[] )
{
    pthread_t th;
    int ret;
    int arg = 10;
    int *thread_ret = NULL;
    ret = pthread_create( &th, NULL, thread, &arg );
    if( ret != 0 ){
        printf( "Create thread error!\n");
        return -1;
    }
    printf( "This is the main process.\n" );
    pthread_join( th, (void**)&thread_ret );
    printf( "thread_ret = %d.\n", *thread_ret );
    return 0;
}

代碼1第一個多線程編程例子
編程

 
將這段代碼保存爲thread.c文件,能夠執行下面的命令來生成可執行文件:
 
$ gcc thread.c -o thread -lpthread
 
這段代碼的執行結果多是這樣:
 
$ ./thread
This is the main process.
This is a thread and arg = 10.
thread_ret = 0.
 
注意,我說的是可能有這樣的結果,在不一樣的環境下可能會有出入。由於這是多線程程序,線程代碼可能先於第24行代碼被執行。
咱們回過頭來再分析一下這段代碼。在第18行調用pthread_create()接口建立了一個新的線程,這個線程的入口函數是start_thread(),而且給這個入口函數傳遞了一個參數,且參數值爲10。這個新建立的線程要執行的任務很是簡單,只是將顯示「This is a thread and arg = 10」這個字符串,由於arg這個參數值已經定義好了,就是10。以後線程將arg參數的值修改成0,並將它做爲線程的返回值返回給系統。與此同時,主進程作的事情就是繼續判斷這個線程是否建立成功了。在咱們的例子中基本上沒有建立失敗的可能。主進程會繼續輸出「This is the main process」字符串,而後調用pthread_join()接口與剛纔的建立進行合併。這個接口的第一個參數就是新建立線程的句柄了,而第二個參數就會去接受線程的返回值。pthread_join()接口會阻塞主進程的執行,直到合併的線程執行結束。因爲線程在結束以後會將0返回給系統,那麼pthread_join()得到的線程返回值天然也就是0。輸出結果「thread_ret = 0」也證明了這一點。
那麼如今有一個問題,那就是pthread_join()接口乾了什麼?什麼是線程合併呢?

2. 線程的合併與分離

 
咱們首先要明確的一個問題就是什麼是線程的合併。從前面的敘述中讀者們已經瞭解到了,pthread_create()接口負責建立了一個線程。那麼線程也屬於系統的資源,這跟內存沒什麼兩樣,並且線程自己也要佔據必定的內存空間。衆所周知的一個問題就是C或C++編程中若是要經過malloc()或new分配了一塊內存,就必須使用free()或delete來回收這塊內存,不然就會產生著名的內存泄漏問題。既然線程和內存沒什麼兩樣,那麼有建立就必須得有回收,不然就會產生另一個著名的資源泄漏問題,這一樣也是一個嚴重的問題。那麼線程的合併就是回收線程資源了。
線程的合併是一種主動回收線程資源的方案。當一個進程或線程調用了針對其它線程的pthread_join()接口,就是線程合併了。這個接口會阻塞調用進程或線程,直到被合併的線程結束爲止。當被合併線程結束,pthread_join()接口就會回收這個線程的資源,並將這個線程的返回值返回給合併者。
與線程合併相對應的另一種線程資源回收機制是線程分離,調用接口是pthread_detach()。線程分離是將線程資源的回收工做交由系統自動來完成,也就是說當被分離的線程結束以後,系統會自動回收它的資源。由於線程分離是啓動系統的自動回收機制,那麼程序也就沒法得到被分離線程的返回值,這就使得pthread_detach()接口只要擁有一個參數就好了,那就是被分離線程句柄。
線程合併和線程分離都是用於回收線程資源的,能夠根據不一樣的業務場景酌情使用。無論有什麼理由,你都必須選擇其中一種,不然就會引起資源泄漏的問題,這個問題與內存泄漏一樣可怕。
線程建立時默認joinable狀態,若是不顯示join阻塞調用或者設置分離狀態,即使線程結束返回或者pthread_exit時都不會釋放線程所佔用堆棧和線程描述符,形成資源泄漏。除了線程建立時設置分離參數以外,還能夠detach。實現方式有兩種:父線程建立完子線程後,執行pthread_detach(tid);或者,子線程代碼裏執行pthread_detach(pthread_self())。
int pthread_detach(pthread_t tid);

3. 線程的屬性

 
前面還說到過線程是有屬性的,這個屬性由一個線程屬性對象來描述。線程屬性對象由pthread_attr_init()接口初始化,並由pthread_attr_destory()來銷燬,它們的完整定義是:
int pthread_attr_init(pthread_attr_t *attr);  
int pthread_attr_destory(pthread_attr_t *attr); 

那麼線程擁有哪些屬性呢?通常地,Linux下的線程有:綁定屬性、分離屬性、調度屬性、堆棧大小屬性和滿佔警惕區大小屬性。下面咱們就分別來介紹這些屬性。安全

3.1 綁定屬性

 
說到這個綁定屬性,就不得不提起另一個概念:輕進程(Light Weight Process,簡稱LWP)。輕進程和Linux系統的內核線程擁有相同的概念,屬於內核的調度實體。一個輕進程能夠控制一個或多個線程。默認狀況下,對於一個擁有n個線程的程序,啓動多少輕進程,由哪些輕進程來控制哪些線程由操做系統來控制,這種狀態被稱爲非綁定的。那麼綁定的含義就很好理解了,只要指定了某個線程「綁」在某個輕進程上,就能夠稱之爲綁定的了。被綁定的線程具備較高的相應速度,由於操做系統的調度主體是輕進程,綁定線程能夠保證在須要的時候它總有一個輕進程可用。綁定屬性就是幹這個用的。
設置綁定屬性的接口是pthread_attr_setscope(),它的完整定義是:
int pthread_attr_setscope(pthread_attr_t *attr, int scope);
它有兩個參數,第一個就是線程屬性對象的指針,第二個就是綁定類型,擁有兩個取值:PTHREAD_SCOPE_SYSTEM(綁定的)和PTHREAD_SCOPE_PROCESS(非綁定的)。代碼2演示了這個屬性的使用。
#include <stdio.h>  
#include <pthread.h>  
……  
int main( int argc, char *argv[] )  
{  
    pthread_attr_t attr;  
    pthread_t th;  
    ……  
    pthread_attr_init( &attr );  
    pthread_attr_setscope( &attr, PTHREAD_SCOPE_SYSTEM );  
    pthread_create( &th, &attr, thread, NULL );  
    ……  
} 
代碼2設置線程綁定屬性
不知道你是否在這裏發現了本文的矛盾之處。就是這個綁定屬性跟咱們以前說的NPTL有矛盾之處。在介紹NPTL的時候就說過業界有一種m:n的線程方案,就跟這個綁定屬性有關。可是筆者還說過NPTL由於Linux的「蠢」沒有采起這種方案,而是採用了「1:1」的方案。這也就是說,Linux的線程永遠都是綁定。對,Linux的線程永遠都是綁定的,因此PTHREAD_SCOPE_PROCESS在Linux中無論用,並且會返回ENOTSUP錯誤。
既然Linux並不支持線程的非綁定,爲何還要提供這個接口呢?答案就是兼容!由於Linux的NTPL是號稱POSIX標準兼容的,而綁定屬性正是POSIX標準所要求的,因此提供了這個接口。若是讀者們只是在Linux下編寫多線程程序,能夠徹底忽略這個屬性。若是哪天你遇到了支持這種特性的系統,別忘了我曾經跟你提及過這玩意兒:)

3.2 分離屬性

 
前面說過線程可以被合併和分離,分離屬性就是讓線程在建立以前就決定它應該是分離的。若是設置了這個屬性,就沒有必要調用pthread_join()或pthread_detach()來回收線程資源了。
設置分離屬性的接口是pthread_attr_setdetachstate(),它的完整定義是:
pthread_attr_setdetachstat(pthread_attr_t *attr, int detachstate); 

它的第二個參數有兩個取值:PTHREAD_CREATE_DETACHED(分離的)和PTHREAD_CREATE_JOINABLE(可合併的,也是默認屬性)。代碼3演示了這個屬性的使用。多線程

#include <stdio.h>  
#include <pthread.h>  
……  
int main( int argc, char *argv[] )  
{  
    pthread_attr_t attr;  
    pthread_t th;  
    ……  
    pthread_attr_init( &attr );  
    pthread_attr_setscope( &attr, PTHREAD_SCOPE_SYSTEM );  
    pthread_create( &th, &attr, thread, NULL );  
    ……  
}
代碼3設置線程分離屬性
 

3.3 調度屬性

 
線程的調度屬性有三個,分別是:算法、優先級和繼承權。
Linux提供的線程調度算法有三個:輪詢、先進先出和其它。其中輪詢和先進先出調度算法是POSIX標準所規定,而其餘則表明採用Linux本身認爲更合適的調度算法,因此默認的調度算法也就是其它了。輪詢和先進先出調度算法都屬於實時調度算法。輪詢指的是時間片輪轉,當線程的時間片用完,系統將從新分配時間片,並將它放置在就緒隊列尾部,這樣能夠保證具備相同優先級的輪詢任務得到公平的CPU佔用時間;先進先出就是先到先服務,一旦線程佔用了CPU則一直運行,直到有更高優先級的線程出現或本身放棄。
設置線程調度算法的接口是pthread_attr_setschedpolicy(),它的完整定義是:
 
pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
 
它的第二個參數有三個取值:SCHED_RR(輪詢)、SCHED_FIFO(先進先出)和SCHED_OTHER(其它)。
Linux的線程優先級與進程的優先級不同,進程優先級咱們後面再說。Linux的線程優先級是從1到99的數值,數值越大表明優先級越高。並且要注意的是,只有採用SHCED_RR或SCHED_FIFO調度算法時,優先級纔有效。對於採用SCHED_OTHER調度算法的線程,其優先級恆爲0。
設置線程優先級的接口是pthread_attr_setschedparam(),它的完整定義是:
struct sched_param {  
    int sched_priority;  
}  
int pthread_attr_setschedparam(pthread_attr_t *attr, struct sched_param *param); 
sched_param結構體的sched_priority字段就是線程的優先級了。
此外,即使採用SCHED_RR或SCHED_FIFO調度算法,線程優先級也不是隨便就能設置的。首先,進程必須是以root帳號運行的;其次,還須要放棄線程的繼承權。什麼是繼承權呢?就是當建立新的線程時,新線程要繼承父線程(建立者線程)的調度屬性。若是不但願新線程繼承父線程的調度屬性,就要放棄繼承權。
設置線程繼承權的接口是pthread_attr_setinheritsched(),它的完整定義是:
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);  
它的第二個參數有兩個取值:PTHREAD_INHERIT_SCHED(擁有繼承權)和PTHREAD_EXPLICIT_SCHED(放棄繼承權)。新線程在默認狀況下是擁有繼承權。
代碼4可以演示不一樣調度算法和不一樣優先級下各線程的行爲,同時也展現如何修改線程的調度屬性。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define THREAD_COUNT 12
void show_thread_policy( int threadno )
{
    int policy;
    struct sched_param param;
    pthread_getschedparam( pthread_self(), &policy, ¶m );
    switch( policy ){
    case SCHED_OTHER:
        printf( "SCHED_OTHER %d\n", threadno );
        break;
    case SCHED_RR:
        printf( "SCHDE_RR %d\n", threadno );
        break;
    case SCHED_FIFO:
        printf( "SCHED_FIFO %d\n", threadno );
        break;
    default:
        printf( "UNKNOWN\n");
    }
}
void* thread( void *arg )
{
    int i, j;
    long threadno = (long)arg;
    printf( "thread %d start\n", threadno );
    sleep(1);
    show_thread_policy( threadno );
    for( i = 0; i < 10; ++i ) {
        for( j = 0; j < 100000000; ++j ){}
        printf( "thread %d\n", threadno );
    }
    printf( "thread %d exit\n", threadno );
    return NULL;
}
int main( int argc, char *argv[] )
{
    long i;
    pthread_attr_t attr[THREAD_COUNT];
    pthread_t pth[THREAD_COUNT];
    struct sched_param param;
    for( i = 0; i < THREAD_COUNT; ++i )
        pthread_attr_init( &attr[i] );
        for( i = 0; i < THREAD_COUNT / 2; ++i ) {
            param.sched_priority = 10;                  
            pthread_attr_setschedpolicy( &attr[i], SCHED_FIFO );
            pthread_attr_setschedparam( &attr[i], ¶m );
            pthread_attr_setinheritsched( &attr[i], PTHREAD_EXPLICIT_SCHED );
        }
        for( i = THREAD_COUNT / 2; i < THREAD_COUNT; ++i ) {
            param.sched_priority = 20;                  
            pthread_attr_setschedpolicy( &attr[i], SCHED_FIFO );
            pthread_attr_setschedparam( &attr[i], ¶m );
            pthread_attr_setinheritsched( &attr[i], PTHREAD_EXPLICIT_SCHED );
        }
        for( i = 0; i < THREAD_COUNT; ++i )                    
            pthread_create( &pth[i], &attr[i], thread, (void*)i );              
        for( i = 0; i < THREAD_COUNT; ++i )                    
            pthread_join( pth[i], NULL );                    
        for( i = 0; i < THREAD_COUNT; ++i )                    
            pthread_attr_destroy( &attr[i] );                   
    return 0;                           
}
代碼4設置線程調度屬性
這段代碼中含有一些沒有介紹過的接口,讀者們可使用Linux的聯機幫助來查看它們的具體用法和做用。

3.4 堆棧大小屬性

 
從前面的這些例子中能夠了解到,線程的主函數與程序的主函數main()有一個很類似的特性,那就是能夠擁有局部變量。雖然同一個進程的線程之間是共享內存空間的,可是它的局部變量確並不共享。緣由就是局部變量存儲在堆棧中,而不一樣的線程擁有不一樣的堆棧。Linux系統爲每一個線程默認分配了8MB的堆棧空間,若是以爲這個空間不夠用,能夠經過修改線程的堆棧大小屬性進行擴容。
修改線程堆棧大小屬性的接口是pthread_attr_setstacksize(),它的完整定義爲:
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); 
它的第二個參數就是堆棧大小了,以字節爲單位。須要注意的是,線程堆棧不能小於16KB,並且儘可能按4KB(32位系統)或2MB(64位系統)的整數倍分配,也就是內存頁面大小的整數倍。此外,修改線程堆棧大小是有風險的,若是你不清楚你在作什麼,最好別動它(其實我很後悔把這麼危險的東西告訴了你:)。

3.5 滿棧警惕區屬性

 
既然線程是有堆棧的,並且還有大小限制,那麼就必定會出現將堆棧用滿的狀況。線程的堆棧用盡是很是危險的事情,由於這可能會致使對內核空間的破壞,一旦被有心人士所利用,後果也不堪設想。爲了防治這類事情的發生,Linux爲線程堆棧設置了一個滿棧警惕區。這個區域通常就是一個頁面,屬於線程堆棧的一個擴展區域。一旦有代碼訪問了這個區域,就會發出SIGSEGV信號進行通知。
雖然滿棧警惕區能夠起到安全做用,可是也有弊病,就是會白白浪費掉內存空間,對於內存緊張的系統會使系統變得很慢。全部就有了關閉這個警惕區的需求。同時,若是咱們修改了線程堆棧的大小,那麼系統會認爲咱們會本身管理堆棧,也會將警惕區取消掉,若是有須要就要開啓它。
修改滿棧警惕區屬性的接口是pthread_attr_setguardsize(),它的完整定義爲:
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
它的第二個參數就是警惕區大小了,以字節爲單位。與設置線程堆棧大小屬性相仿,應該儘可能按照4KB或2MB的整數倍來分配。當設置警惕區大小爲0時,就關閉了這個警惕區。
雖然棧滿警惕區須要浪費掉一點內存,可是可以極大的提升安全性,因此這點損失是值得的。並且一旦修改了線程堆棧的大小,必定要記得同時設置這個警惕區。

4. 線程本地存儲

 
內線程之間能夠共享內存地址空間,線程之間的數據交換能夠很是快捷,這是線程最顯著的優勢。可是多個線程訪問共享數據,須要昂貴的同步開銷,也容易形成與同步相關的BUG,更麻煩的是有些數據根本就不但願被共享,這又是缺點。可謂:「成也蕭何,敗也蕭何」,說的就是這個道理。
C程序庫中的errno是個最典型的一個例子。errno是一個全局變量,會保存最後一個系統調用的錯誤代碼。在單線程環境並不會出現什麼問題。可是在多線程環境,因爲全部線程都會有可能修改errno,這就很難肯定errno表明的究竟是哪一個系統調用的錯誤代碼了。這就是有名的「非線程安全(Non Thread-Safe)」的。
此外,從現代技術角度看,在不少時候使用多線程的目的並非爲了對共享數據進行並行處理(在Linux下有更好的方案,後面會介紹)。更可能是因爲多核心CPU技術的引入,爲了充分利用CPU資源而進行並行運算(不互相干擾)。換句話說,大多數狀況下每一個線程只會關心本身的數據而不須要與別人同步。
爲了解決這些問題,能夠有不少種方案。好比使用不一樣名稱的全局變量。可是像errno這種名稱已經固定了的全局變量就沒辦法了。在前面的內容中提到在線程堆棧中分配局部變量是不在線程間共享的。可是它有一個弊病,就是線程內部的其它函數很難訪問到。目前解決這個問題的簡便易行的方案是線程本地存儲,即Thread Local Storage,簡稱TLS。利用TLS,errno所反映的就是本線程內最後一個系統調用的錯誤代碼了,也就是線程安全的了。
Linux提供了對TLS的完整支持,經過下面這些接口來實現:
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));  
int pthread_key_delete(pthread_key_t key);  
void* pthread_getspecific(pthread_key_t key);  
int pthread_setspecific(pthread_key_t key, const void *value);
pthread_key_create()接口用於建立一個線程本地存儲區。第一個參數用來返回這個存儲區的句柄,須要使用一個全局變量保存,以便全部線程都能訪問到。第二個參數是線程本地數據的回收函數指針,若是但願本身控制線程本地數據的生命週期,這個參數能夠傳遞NULL。
pthread_key_delete()接口用於回收線程本地存儲區。其惟一的參數就要回收的存儲區的句柄。
pthread_getspecific()和pthread_setspecific()這個兩個接口分別用於獲取和設置線程本地存儲區的數據。這兩個接口在不一樣的線程下會有不一樣的結果不一樣(相同的線程下就會有相同的結果),這也就是線程本地存儲的關鍵所在。
代碼5展現瞭如何在Linux使用線程本地存儲,注意執行結果,分析一下線程本地存儲的一些特性,以及內存回收的時機。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define THREAD_COUNT 10
pthread_key_t g_key;
typedef struct thread_data{
    int thread_no;
} thread_data_t;
void show_thread_data()
{
    thread_data_t *data = pthread_getspecific( g_key );
    printf( "Thread %d \n", data->thread_no );
}
void* thread( void *arg )
{
    thread_data_t *data = (thread_data_t *)arg;
    printf( "Start thread %d\n", data->thread_no );
    pthread_setspecific( g_key, data );
    show_thread_data();
    printf( "Thread %d exit\n", data->thread_no );
}
void free_thread_data( void *arg )
{
    thread_data_t *data = (thread_data_t*)arg;
    printf( "Free thread %d data\n", data->thread_no );
    free( data );
}
int main( int argc, char *argv[] )
{
    int i;
    pthread_t pth[THREAD_COUNT];
    thread_data_t *data = NULL;
    pthread_key_create( &g_key, free_thread_data );
    for( i = 0; i < THREAD_COUNT; ++i ) {
        data = malloc( sizeof( thread_data_t ) );
        data->thread_no = i;
        pthread_create( &pth[i], NULL, thread, data );
    }
    for( i = 0; i < THREAD_COUNT; ++i )
        pthread_join( pth[i], NULL );
    pthread_key_delete( g_key );
    return 0;
}
代碼5使用線程本地存儲

5. 線程的同步

 
雖然線程本地存儲能夠避免線程訪問共享數據,可是線程之間的大部分數據始終仍是共享的。在涉及到對共享數據進行讀寫操做時,就必須使用同步機制,不然就會形成線程們哄搶共享數據的結果,這會把你的數據弄的七零八落理不清頭緒。
Linux提供的線程同步機制主要有互斥鎖和條件變量。其它形式的線程同步機制用得並很少,本書也不許備詳細講解,有興趣的讀者能夠參考相關文檔。

5.1 互斥鎖

 
首先咱們看一下互斥鎖。所謂的互斥就是線程之間互相排斥,得到資源的線程排斥其它沒有得到資源的線程。Linux使用互斥鎖來實現這種機制。
既然叫鎖,就有加鎖和解鎖的概念。當線程得到了加鎖的資格,那麼它將獨享這個鎖,其它線程一旦試圖去碰觸這個鎖就當即被系統「拍暈」。當加鎖的線程解開並放棄了這個鎖以後,那些被「拍暈」的線程會被系統喚醒,而後繼續去爭搶這個鎖。至於誰能搶到,只有天知道。可是總有一個能搶到。因而其它來湊熱鬧的線程又被系統給「拍暈」了……如此反覆。感受線程的「頭」很痛:)
從互斥鎖的這種行爲看,線程加鎖和解鎖之間的代碼至關於一個獨木橋,贊成時刻只有一個線程能執行。從全局上看,在這個地方,全部並行運行的線程都變成了排隊運行了。比較專業的叫法是同步執行,這段代碼區域叫臨界區。同步執行就破壞了線程並行性的初衷了,臨界區越大破壞得越厲害。因此在實際應用中,應該儘可能避免有臨界區出現。實在不行,臨界區也要儘可能的小。若是連縮小臨界區都作不到,那還使用多線程幹嗎?
互斥鎖在Linux中的名字是mutex。這個彷佛優勢眼熟。對,在前面介紹NPTL的時候提起過,可是那個叫futex,是系統底層機制。對於提供給用戶使用的則是這個mutex。Linux初始化和銷燬互斥鎖的接口是pthread_mutex_init()和pthead_mutex_destroy(),對於加鎖和解鎖則有pthread_mutex_lock()、pthread_mutex_trylock()和pthread_mutex_unlock()。這些接口的完整定義以下:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);  
int pthread_mutex_destory(pthread_mutex_t *mutex );  
int pthread_mutex_lock(pthread_mutex_t *mutex);  
int pthread_mutex_trylock(pthread_mutex_t *mutex);  
int pthread_mutex_unlock(pthread_mutex_t *mutex); 
從這些定義中能夠看到,互斥鎖也是有屬性的。只不過這個屬性在絕大多數狀況下都不須要改動,因此使用默認的屬性就行。方法就是給它傳遞NULL。
phtread_mutex_trylock()比較特別,用它試圖加鎖的線程永遠都不會被系統「拍暈」,只是經過返回EBUSY來告訴程序員這個鎖已經有人用了。至因而否繼續「強闖」臨界區,則由程序員決定。系統提供這個接口的目的可不是讓線程「強闖」臨界區的。它的根本目的仍是爲了提升並行性,留着這個線程去幹點其它有意義的事情。固然,若是很幸運恰巧這個時候尚未人擁有這把鎖,那麼天然也會取得臨界區的使用權。
代碼6演示了在Linux下如何使用互斥鎖。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t g_mutex;
int g_lock_var = 0;
void* thread1( void *arg )
{
    int i, ret;
    time_t end_time;
    end_time = time(NULL) + 10;
    while( time(NULL) < end_time ) {
        ret = pthread_mutex_trylock( &g_mutex );
        if( EBUSY == ret ) {
            printf( "thread1: the varible is locked by thread2.\n" );
        } else {
            printf( "thread1: lock the variable!\n" );
            ++g_lock_var;
            pthread_mutex_unlock( &g_mutex );
        }
        sleep(1);
    }
    return NULL;
}
void* thread2( void *arg )
{
    int i;
    time_t end_time;
    end_time = time(NULL) + 10;
    while( time(NULL) < end_time ) {
        pthread_mutex_lock( &g_mutex );
        printf( "thread2: lock the variable!\n" );
        ++g_lock_var;
        sleep(1);
        pthread_mutex_unlock( &g_mutex );
    }
    return NULL;
}
int main( int argc, char *argv[] )
{
    int i;
    pthread_t pth1,pth2;
    pthread_mutex_init( &g_mutex, NULL );
    pthread_create( &pth1, NULL, thread1, NULL );
    pthread_create( &pth2, NULL, thread2, NULL );
    pthread_join( pth1, NULL );
    pthread_join( pth2, NULL );
    pthread_mutex_destroy( &g_mutex );
    printf( "g_lock_var = %d\n", g_lock_var );
    return 0;                            
}
代碼6使用互斥鎖
 
最後須要補充一點,互斥鎖在同一個線程內,沒有互斥的特性。也就是說,線程不能利用互斥鎖讓系統將本身「拍暈」。解釋這個現象的一個很好的理由就是,擁有鎖的線程把本身「拍暈」了,誰還能再擁有這把鎖呢?可是另外狀況須要避免,就是兩個線程已經各自擁有一把鎖了,可是還想獲得對方的鎖,這個時候兩個線程都會被「拍暈」。一旦這種狀況發生,就誰都不能得到這個鎖了,這種狀況還有一個著名的名字——死鎖。死鎖是永遠都要避免的事情,由於這是嚴重損人不利己的行爲。

5.2 條件變量

 
條件變量關鍵點在「變量」上。與鎖的不一樣之處就是,當線程遇到這個「變量」,並非相似鎖那樣的被系統給「拍暈」,而是根據「條件」來選擇是否在那裏等待。等待什麼呢?等待容許經過的「信號」。這個「信號」是系統控制的嗎?顯然不是!它是由另一個線程來控制的。
若是說互斥鎖能夠比做獨木橋,那麼條件變量這就比如是馬路上的紅綠燈。車輛遇到紅綠燈確定會根據「燈」的顏色來判斷是否通行,畢竟紅燈停綠燈行這個道理在幼兒園的時候老師就教了。那麼誰來控制「燈」的顏色呢?必定是交警啊,至少你我都不敢動它(有人會說那是自動的,但是間隔多少時間變換也是交警設置不是?)。那麼「車輛」和「交警」就是馬路上的兩類線程,大多數狀況下都是「車」多「交警」少。
更深一步理解,條件變量是一種事件機制。由一類線程來控制「事件」的發生,另一類線程等待「事件」的發生。爲了實現這種機制,條件變量必須是共享於線程之間的全局變量。並且,條件變量也須要與互斥鎖同時使用。
初始化和銷燬條件變量的接口是pthread_cond_init()和pthread_cond_destory();控制「事件」發生的接口是pthread_cond_signal()或pthread_cond_broadcast();等待「事件」發生的接口是pthead_cond_wait()或pthread_cond_timedwait()。它們的完整定義以下:
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);  
int pthread_cond_destory(pthread_cond_t *cond);  
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);  
int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex, const timespec *abstime);  
int pthread_cond_signal(pthread_cond_t *cond);  
int pthread_cond_broadcast(pthread_cond_t *cond); 
對於等待「事件」的接口從其名稱中能夠看出,一種是無限期等待,一種是限時等待。後者與互斥鎖的pthread_mutex_trylock()有些相似,即當等待的「事件」通過一段時間以後依然沒有發生,那就去幹點別的有意義的事情去。而對於控制「事件」發生的接口則有「單播」和「廣播」之說。所謂單播就是隻有一個線程會獲得「事件」已經發生了的「通知」,而廣播就是全部線程都會獲得「通知」。對於廣播狀況,全部被「通知」到的線程也要通過由互斥鎖控制的獨木橋。
對於條件變量的使用,能夠參考代碼7,它實現了一種生產者與消費者的線程同步方案。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define BUFFER_SIZE 5
pthread_mutex_t g_mutex;
pthread_cond_t g_cond;
typedef struct {
    char buf[BUFFER_SIZE];
    int count;
} buffer_t;
buffer_t g_share = {"", 0};
char g_ch = 'A';
void* producer( void *arg )
{
    printf( "Producer starting.\n" );
    while( g_ch != 'Z' ) {
        pthread_mutex_lock( &g_mutex );
        if( g_share.count < BUFFER_SIZE ) {
            g_share.buf[g_share.count++] = g_ch++;
            printf( "Prodcuer got char[%c]\n", g_ch - 1 );
            if( BUFFER_SIZE == g_share.count ) {
                printf( "Producer signaling full.\n" );
                pthread_cond_signal( &g_cond );
            }
        }
        pthread_mutex_unlock( &g_mutex );
    }
    printf( "Producer exit.\n" );
    return NULL;
}
void* consumer( void *arg )
{
    int i;
    printf( "Consumer starting.\n" );
    while( g_ch != 'Z' ) {
        pthread_mutex_lock( &g_mutex );
        printf( "Consumer waiting\n" );
        pthread_cond_wait( &g_cond, &g_mutex );
        printf( "Consumer writing buffer\n" );
        for( i = 0; g_share.buf[i] && g_share.count; ++i ) {
            putchar( g_share.buf[i] );
            --g_share.count;
        }
        putchar('\n');
        pthread_mutex_unlock( &g_mutex );
    }
    printf( "Consumer exit.\n" );
    return NULL;
}
int main( int argc, char *argv[] )
{
    pthread_t ppth, cpth;
    pthread_mutex_init( &g_mutex, NULL );
    pthread_cond_init( &g_cond, NULL );
    pthread_create( &cpth, NULL, consumer, NULL );
    pthread_create( &ppth, NULL, producer, NULL );
    pthread_join( ppth, NULL );
    pthread_join( cpth, NULL );
    pthread_mutex_destroy( &g_mutex );
    pthread_cond_destroy( &g_cond );
    return 0;
}
代碼7使用條件變量
從代碼中會發現,等待「事件」發生的接口都須要傳遞一個互斥鎖給它。而實際上這個互斥鎖還要在調用它們以前加鎖,調用以後解鎖。不單如此,在調用操做「事件」發生的接口以前也要加鎖,調用以後解鎖。這就面臨一個問題,按照這種方式,等於「發生事件」和「等待事件」是互爲臨界區的。也就是說,若是「事件」尚未發生,那麼有線程要等待這個「事件」就會阻止「事件」的發生。更乾脆一點,就是這個「生產者」和「消費者」是在來回的走獨木橋。可是實際的狀況是,「消費者」在緩衝區滿的時候會獲得這個「事件」的「通知」,而後將字符逐個打印出來,並清理緩衝區。直到緩衝區的全部字符都被打印出來以後,「生產者」纔開始繼續工做。
爲何會有這樣的結果呢?這就要說明一下pthread_cond_wait()接口對互斥鎖作什麼。答案是:解鎖。pthread_cond_wait()首先會解鎖互斥鎖,而後進入等待。這個時候「生產者」就可以進入臨界區,而後在條件知足的時候向「消費者」發出信號。當pthead_cond_wait()得到「通知」以後,它還要對互斥鎖加鎖,這樣能夠防止「生產者」繼續工做而「撐壞」緩衝區。另外,「生產者」在緩衝區不滿的狀況下才能工做的這個限定條件是頗有必要的。由於在pthread_cond_wait()得到通知以後,在沒有對互斥鎖加鎖以前,「生產者」可能已經從新進入臨界區了,這樣「消費者」又被堵住了。也就是由於條件變量這種工做性質,致使它必須與互斥鎖聯合使用。

此外,利用條件變量和互斥鎖,能夠模擬出不少其它類型的線程同步機制,好比:event、semaphore等。函數

相關文章
相關標籤/搜索