在Linux中使用線程

我並不假定你會使用Linux的線程,所以在這裏就簡單的介紹一下。如果你之前有過多線程方面的編程經驗,完全可以忽略本文的內容,因爲它非常的初級。

首先說明一下,在Linux編寫多線程程序需要包含頭文件pthread.h。也就是說你在任何採用多線程設計的程序中都會看到類似這樣的代碼:
1
#include <pthread.h>
當然,進包含一個頭文件是不能搞定線程的,還需要連接libpthread.so這個庫,因此在程序連接階段應該有類似這樣的指令:
gcc program.o -o program -lpthread

1. 第一個例子

在Linux下創建的線程的API接口是pthread_create(),它的完整定義是:
1
int pthread_create(pthread_t *threadconst 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所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <pthread.h>
voidthreadvoid *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()接口只要擁有一個參數就行了,那就是被分離線程句柄。
線程合併和線程分離都是用於回收線程資源的,可以根據不同的業務場景酌情使用。不管有什麼理由,你都必須選擇其中一種,否則就會引發資源泄漏的問題,這個問題與內存泄漏同樣可怕。

3. 線程的屬性

前面還說到過線程是有屬性的,這個屬性由一個線程屬性對象來描述。線程屬性對象由pthread_attr_init()接口初始化,並由pthread_attr_destory()來銷燬,它們的完整定義是:
1
2
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演示了這個屬性的使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
#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演示了這個屬性的使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
#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(),它的完整定義是:
1
2
3
4
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(),它的完整定義是:
1
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);
它的第二個參數有兩個取值:PTHREAD_INHERIT_SCHED(擁有繼承權)和PTHREAD_EXPLICIT_SCHED(放棄繼承權)。新線程在默認情況下是擁有繼承權。
代碼4能夠演示不同調度算法和不同優先級下各線程的行爲,同時也展示如何修改線程的調度屬性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#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 );
    ……
}
#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, &param );
    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");
    }
}
voidthreadvoid *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], &param );
            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], &param );
            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(),它的完整定義爲:
1
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
它的第二個參數就是堆棧大小了,以字節爲單位。需要注意的是,線程堆棧不能小於16KB,而且儘量按4KB(32位系統)或2MB(64位系統)的整數倍分配,也就是內存頁面大小的整數倍。此外,修改線程堆棧大小是有風險的,如果你不清楚你在做什麼,最好別動它(其實我很後悔把這麼危險的東西告訴了你:)。

3.5 滿棧警戒區屬性

既然線程是有堆棧的,而且還有大小限制,那麼就一定會出現將堆棧用滿的情況。線程的堆棧用滿是非常危險的事情,因爲這可能會導致對內核空間的破壞,一旦被有心人士所利用,後果也不堪設想。爲了防治這類事情的發生,Linux爲線程堆棧設置了一個滿棧警戒區。這個區域一般就是一個頁面,屬於線程堆棧的一個擴展區域。一旦有代碼訪問了這個區域,就會發出SIGSEGV信號進行通知。
雖然滿棧警戒區可以起到安全作用,但是也有弊病,就是會白白浪費掉內存空間,對於內存緊張的系統會使系統變得很慢。所有就有了關閉這個警戒區的需求。同時,如果我們修改了線程堆棧的大小,那麼系統會認爲我們會自己管理堆棧,也會將警戒區取消掉,如果有需要就要開啓它。
修改滿棧警戒區屬性的接口是pthread_attr_setguardsize(),它的完整定義爲:
1
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的完整支持,通過下面這些接口來實現:
1
2
3
4
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使用線程本地存儲,注意執行結果,分析一下線程本地存儲的一些特性,以及內存回收的時機。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#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 );
}
voidthreadvoid *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 = mallocsizeof( 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()。這些接口的完整定義如下:
1
2
3
4
5
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下如何使用互斥鎖。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#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;
    whiletime(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;
    whiletime(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()。它們的完整定義如下:
1
2
3
4
5
6
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,它實現了一種生產者與消費者的線程同步方案。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#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等。本書不再多講,有興趣的讀者可以參考其它著作。或自己根據它們的行爲自己來模擬實現。

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