Linux下的多執行緒程式設計

jxh_123發表於2015-05-15


作者:gnuhpc
出處:
http://www.cnblogs.com/gnuhpc/

本文作者: 姚繼鋒 (2001-08-11 09:05:00) 黃鵬程(2009-03-13) converse (2009-01-15)
1 引言
執行緒(thread)技術早在60年代就被提出,但真正應用多執行緒到作業系統中去,是在80年代中期,solaris是這方面的佼佼者。傳統的 Unix也支援執行緒的概念,但是在一個程式(process)中只允許有一個執行緒,這樣多執行緒就意味著多程式。現在,多執行緒技術已經被許多作業系統所支援,包括Windows也包括Linux。
為什麼有了程式的概念後,還要再引入執行緒呢?使用多執行緒到底有哪些好處?什麼的系統應該選用多執行緒?我們首先必須回答這些問題。
使用多執行緒的理由之一是和程式相比,它是一種非常"節儉"的多工操作方式。我們知道,在Linux系統下,啟動一個新的程式必須分配給它獨立的地址空間,建立眾多的資料表來維護它的程式碼段、堆疊段和資料段,這是一種"昂貴"的多工工作方式。而執行於一個程式中的多個執行緒,它們彼此之間使用相同的地址空間,共享大部分資料,啟動一個執行緒所花費的空間遠遠小於啟動一個程式所花費的空間,而且,執行緒間彼此切換所需的時間也遠遠小於程式間切換所需要的時間。據統計,總的說來,一個程式的開銷大約是一個執行緒開銷的30倍左右,當然,在具體的系統上,這個資料可能會有較大的區別。
使用多執行緒的理由之二是執行緒間方便的通訊機制。對不同程式來說,它們具有獨立的資料空間,要進行資料的傳遞只能通過通訊的方式進行,這種方式不僅費時,而且很不方便。執行緒則不然,由於同一程式下的執行緒之間共享資料空間,所以一個執行緒的資料可以直接為其它執行緒所用,這不僅快捷,而且方便。當然,資料的共享也帶來其他一些問題,有的變數不能同時被兩個執行緒所修改,有的子程式中宣告為static的資料更有可能給多執行緒程式帶來災難性的打擊,這些正是編寫多執行緒程式時最需要注意的地方。
除了以上所說的優點外,不和程式比較,多執行緒程式作為一種多工、併發的工作方式,當然有以下的優點:
1) 提高應用程式響應。這對圖形介面的程式尤其有意義,當一個操作耗時很長時,整個系統都會等待這個操作,此時程式不會響應鍵盤、滑鼠、選單的操作,而使用多執行緒技術,將耗時長的操作(time consuming)置於一個新的執行緒,可以避免這種尷尬的情況。
2) 使多CPU系統更加有效。作業系統會保證當執行緒數不大於CPU數目時,不同的執行緒執行於不同的CPU上。
3) 改善程式結構。一個既長又複雜的程式可以考慮分為多個執行緒,成為幾個獨立或半獨立的執行部分,這樣的程式會利於理解和修改。
下面我們先來嘗試編寫一個簡單的多執行緒程式。
2 簡單的多執行緒程式設計
Linux系統下的多執行緒遵循POSIX執行緒介面,稱為pthread。編寫Linux下的多執行緒程式,需要使用標頭檔案pthread.h,連線時需要使用庫libpthread.a。順便說一下,Linux下pthread的實現是通過系統呼叫clone()來實現的。clone()是Linux所特有的系統呼叫,它的使用方式類似fork,關於clone()的詳細情況,有興趣的讀者可以去檢視有關文件說明。下面我們展示一個最簡單的多執行緒程式 example1.c。

<span style="font-size:18px;">/*
* =====================================================================================
*
* Filename: pthread1.c
*
* Description: A Simple program of showing What pthread is
*
* Version: 1.0
* Created: 03/10/2009 08:53:48 PM
* Revision: none
* Compiler: gcc
*
* Author: Futuredaemon (BUPT), gnuhpc@gmail.com
* Company: BUPT_UNITED
*
* =====================================================================================
*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *thread(void *threadid)
{
int tid;
tid = (int)threadid;
printf("Hello World! It's me, thread #%d!/n", tid);
pthread_exit(NULL);
}
int main(void)
{
pthread_t id;
void *ret;
int i,retv;
int t=123;
retv=pthread_create(&id,NULL,(void *) thread,(void *)t);
if (retv!=0)
{
printf ("Create pthread error!/n");
return 1;
}
for (i=0;i<3;i++)
printf("This is the main process./n");
pthread_join(id,&ret);
printf("The thread return value is%d/n",(int)ret);
return 0;
}
	</span>

上面的示例中,我們使用到了兩個函式, pthread_create和pthread_join,並宣告瞭一個pthread_t型的變數。
pthread_t在標頭檔案/usr/include/bits/pthreadtypes.h中定義:
typedef unsigned long int pthread_t;
它是一個執行緒的識別符號。函式pthread_create用來建立一個執行緒,它的原型為:
extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,
void *(*__start_routine) (void *), void *__arg));
第一個引數為指向執行緒識別符號的指標,第二個引數用來設定執行緒屬性,第三個引數是執行緒執行函式的起始地址,最後一個引數是執行函式的引數。這裡,我們的函式thread不需要引數,所以最後一個引數設為空指標。第二個引數我們也設為空指標,這樣將生成預設屬性的執行緒。對執行緒屬性的設定和修改我們將在下一節闡述。當建立執行緒成功時,函式返回0,若不為0則說明建立執行緒失敗,常見的錯誤返回程式碼為EAGAIN和EINVAL。前者表示系統限制建立新的執行緒,例如執行緒數目過多了;後者表示第二個引數代表的執行緒屬性值非法。建立執行緒成功後,新建立的執行緒則執行引數三和引數四確定的函式,原來的執行緒則繼續執行下一行程式碼。
函式pthread_join用來等待一個執行緒的結束。函式原型為:
extern int pthread_join __P ((pthread_t __th, void **__thread_return));
第一個引數為被等待的執行緒識別符號,第二個引數為一個使用者定義的指標,它可以用來儲存被等待執行緒的返回值。這個函式是一個執行緒阻塞的函式,呼叫它的函式將一直等待到被等待的執行緒結束為止,當函式返回時,被等待執行緒的資源被收回。一個執行緒的結束有兩種途徑,一種是象我們上面的例子一樣,函式結束了,呼叫它的執行緒也就結束了;另一種方式是通過函式pthread_exit來實現。它的函式原型為:
extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
唯一的引數是函式的返回程式碼,只要pthread_join中的第二個引數thread_return不是NULL,這個值將被傳遞給 thread_return。最後要說明的是,一個執行緒不能被多個執行緒等待,否則第一個接收到訊號的執行緒成功返回,其餘呼叫pthread_join的執行緒則返回錯誤程式碼ESRCH。
在這一節裡,我們編寫了一個最簡單的執行緒,並掌握了最常用的三個函式pthread_create,pthread_join和pthread_exit。下面,我們來了解執行緒的一些常用屬性以及如何設定這些屬性。
3 修改執行緒的屬性
在上一節的例子裡,我們用pthread_create函式建立了一個執行緒,在這個執行緒中,我們使用了預設引數,即將該函式的第二個引數設為NULL。的確,對大多數程式來說,使用預設屬性就夠了,但我們還是有必要來了解一下執行緒的有關屬性。
屬性結構為pthread_attr_t,它同樣在標頭檔案/usr/include/pthread.h中定義,喜歡追根問底的人可以自己去檢視。屬性值不能直接設定,須使用相關函式進行操作,初始化的函式為pthread_attr_init,這個函式必須在pthread_create函式之前呼叫。屬性物件主要包括是否繫結、是否分離、堆疊地址、堆疊大小、優先順序。預設的屬性為非繫結、非分離、預設1M的堆疊、與父程式同樣級別的優先順序。
關於執行緒的繫結,牽涉到另外一個概念:輕程式(LWP:Light Weight Process)。輕程式可以理解為核心執行緒,它位於使用者層和系統層之間。系統對執行緒資源的分配、對執行緒的控制是通過輕程式來實現的,一個輕程式可以控制一個或多個執行緒。預設狀況下,啟動多少輕程式、哪些輕程式來控制哪些執行緒是由系統來控制的,這種狀況即稱為非繫結的。繫結狀況下,則顧名思義,即某個執行緒固定的"綁"在一個輕程式之上。被繫結的執行緒具有較高的響應速度,這是因為CPU時間片的排程是面向輕程式的,繫結的執行緒可以保證在需要的時候它總有一個輕程式可用。通過設定被繫結的輕程式的優先順序和排程級可以使得繫結的執行緒滿足諸如實時反應之類的要求。
設定執行緒繫結狀態的函式為 pthread_attr_setscope,它有兩個引數,第一個是指向屬性結構的指標,第二個是繫結型別,它有兩個取值:PTHREAD_SCOPE_SYSTEM(繫結的)和PTHREAD_SCOPE_PROCESS(非繫結的)。下面的程式碼即建立了一個繫結的執行緒。
#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;
/*初始化屬性值,均設為預設值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid, &attr, (void *) my_function, NULL);
執行緒的分離狀態決定一個執行緒以什麼樣的方式來終止自己。在上面的例子中,我們採用了執行緒的預設屬性,即為非分離狀態,這種情況下,原有的執行緒等待建立的執行緒結束。只有當pthread_join()函式返回時,建立的執行緒才算終止,才能釋放自己佔用的系統資源。而分離執行緒不是這樣子的,它沒有被其他的執行緒所等待,自己執行結束了,執行緒也就終止了,馬上釋放系統資源。程式設計師應該根據自己的需要,選擇適當的分離狀態。設定執行緒分離狀態的函式為 pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二個引數可選為PTHREAD_CREATE_DETACHED(分離執行緒)和 PTHREAD _CREATE_JOINABLE(非分離執行緒)。這裡要注意的一點是,如果設定一個執行緒為分離執行緒,而這個執行緒執行又非常快,它很可能在 pthread_create函式返回之前就終止了,它終止以後就可能將執行緒號和系統資源移交給其他的執行緒使用,這樣呼叫pthread_create的執行緒就得到了錯誤的執行緒號。要避免這種情況可以採取一定的同步措施,最簡單的方法之一是可以在被建立的執行緒裡呼叫 pthread_cond_timewait函式,讓這個執行緒等待一會兒,留出足夠的時間讓函式pthread_create返回。設定一段等待時間,是在多執行緒程式設計裡常用的方法。但是注意不要使用諸如wait()之類的函式,它們是使整個程式睡眠,並不能解決執行緒同步的問題。
另外一個可能常用的屬性是執行緒的優先順序,它存放在結構sched_param中。用函式pthread_attr_getschedparam和函式 pthread_attr_setschedparam進行存放,一般說來,我們總是先取優先順序,對取得的值修改後再存放回去。下面即是一段簡單的例子。
#include <pthread.h>
#include <sched.h>
pthread_attr_t attr;
pthread_t tid;
sched_param param;
int newprio=20;
pthread_attr_init(&attr);
pthread_attr_getschedparam(&attr, &param);
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr, &param);
pthread_create(&tid, &attr, (void *)myfunction, myarg);
4 執行緒的資料處理
和程式相比,執行緒的最大優點之一是資料的共享性,各個程式共享父程式處沿襲的資料段,可以方便的獲得、修改資料。但這也給多執行緒程式設計帶來了許多問題。我們必須當心有多個不同的程式訪問相同的變數。許多函式是不可重入的,即同時不能執行一個函式的多個拷貝(除非使用不同的資料段)。在函式中宣告的靜態變數常常帶來問題,函式的返回值也會有問題。因為如果返回的是函式內部靜態宣告的空間的地址,則在一個執行緒呼叫該函式得到地址後使用該地址指向的資料時,別的執行緒可能呼叫此函式並修改了這一段資料。在程式中共享的變數必須用關鍵字volatile來定義,這是為了防止編譯器在優化時(如gcc中使用-OX引數)改變它們的使用方式。為了保護變數,我們必須使用訊號量、互斥等方法來保證我們對變數的正確使用。下面,我們就逐步介紹處理執行緒資料時的有關知識。
4.1 執行緒資料
在單執行緒的程式裡,有兩種基本的資料:全域性變數和區域性變數。但在多執行緒程式裡,還有第三種資料型別:執行緒資料(TSD: Thread-Specific Data)。它和全域性變數很象,線上程內部,各個函式可以象使用全域性變數一樣呼叫它,但它對執行緒外部的其它執行緒是不可見的。這種資料的必要性是顯而易見的。例如我們常見的變數errno,它返回標準的出錯資訊。它顯然不能是一個區域性變數,幾乎每個函式都應該可以呼叫它;但它又不能是一個全域性變數,否則在 A執行緒裡輸出的很可能是B執行緒的出錯資訊。要實現諸如此類的變數,我們就必須使用執行緒資料。我們為每個執行緒資料建立一個鍵,它和這個鍵相關聯,在各個執行緒裡,都使用這個鍵來指代執行緒資料,但在不同的執行緒裡,這個鍵代表的資料是不同的,在同一個執行緒裡,它代表同樣的資料內容。
和執行緒資料相關的函式主要有4個:建立一個鍵;為一個鍵指定執行緒資料;從一個鍵讀取執行緒資料;刪除鍵。
建立鍵的函式原型為:
extern int pthread_key_create __P ((pthread_key_t *__key,void (*__destr_function) (void *)));
第一個引數為指向一個鍵值的指標,第二個引數指明瞭一個destructor函式,如果這個引數不為空,那麼當每個執行緒結束時,系統將呼叫這個函式來釋放繫結在這個鍵上的記憶體塊。這個函式常和函式pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,為了讓這個鍵只被建立一次。函式pthread_once宣告一個初始化函式,第一次呼叫pthread_once時它執行這個函式,以後的呼叫將被它忽略。
在下面的例子中,我們建立一個鍵,並將它和某個資料相關聯。我們要定義一個函式 createWindow,這個函式定義一個圖形視窗(資料型別為Fl_Window *,這是圖形介面開發工具FLTK中的資料型別)。由於各個執行緒都會呼叫這個函式,所以我們使用執行緒資料。
/* 宣告一個鍵*/
pthread_key_t myWinKey;
/* 函式 createWindow */
void createWindow ( void ) {
Fl_Window * win;
static pthread_once_t once=PTHREAD_ONCE_INIT;
/* 呼叫函式createMyKey,建立鍵*/
pthread_once ( & once, createMyKey) ;
/*win指向一個新建立的視窗*/
win=new Fl_Window( 0, 0, 100, 100, "MyWindow");
/* 對此視窗作一些可能的設定工作,如大小、位置、名稱等*/
setWindow(win);
/* 將視窗指標值繫結在鍵myWinKey上*/
pthread_setpecific ( myWinKey, win);
}
/* 函式 createMyKey,建立一個鍵,並指定了destructor */
void createMyKey ( void ) {
pthread_keycreate(&myWinKey, freeWinKey);
}
/* 函式 freeWinKey,釋放空間*/
void freeWinKey ( Fl_Window * win){
delete win;
}
這樣,在不同的執行緒中呼叫函式createMyWin,都可以得到線上程內部均可見的視窗變數,這個變數通過函式 pthread_getspecific得到。在上面的例子中,我們已經使用了函式pthread_setspecific來將執行緒資料和一個鍵繫結在一起。這兩個函式的原型如下:
extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));
extern void *pthread_getspecific __P ((pthread_key_t __key));
這兩個函式的引數意義和使用方法是顯而易見的。要注意的是,pthread_setspecific為一個鍵指定新的執行緒資料時,必須自己釋放原有的執行緒資料以回收空間。這個過程函式pthread_key_delete用來刪除一個鍵,這個鍵佔用的記憶體將被釋放,但同樣要注意的是,它只釋放鍵佔用的記憶體,並不釋放該鍵關聯的執行緒資料所佔用的記憶體資源,而且它也不會觸發函式pthread_key_create中定義的destructor函式。執行緒資料的釋放必須在釋放鍵之前完成。
4.2 互斥鎖
互斥鎖用來保證一段時間內只有一個執行緒在執行一段程式碼。必要性顯而易見:假設各個執行緒向同一個檔案順序寫入資料,最後得到的結果一定是災難性的。
我們先看下面一段程式碼。這是一個讀/寫程式,它們公用一個緩衝區,並且我們假定一個緩衝區只能儲存一條資訊。即緩衝區只有兩個狀態:有資訊或沒有資訊。

<span style="font-size:18px;">/*
* =====================================================================================
*
* Filename: pthread2.c
*
* Description: A Program of mutex
*
* Version: 1.0
* Created: 03/11/2009 08:32:51 PM
* Revision: none
* Compiler: gcc
*
* Author: Futuredaemon (BUPT), gnuhpc@gmail.com
* Company: BUPT_UNITED
*
* =====================================================================================
*/
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void reader_function ( void );
void writer_function ( void );
int buffer_has_item=0;
pthread_mutex_t mutex;
int main ( void )
{
pthread_t reader;
pthread_mutex_init (&mutex,NULL);
pthread_create(&reader, NULL, (void *)&reader_function, NULL);
writer_function( );
return 0;
}
void writer_function (void)
{
while (1)
{
pthread_mutex_lock (&mutex);
if (buffer_has_item==0)
{
buffer_has_item=1;
printf("Write once!/n");
}
pthread_mutex_unlock(&mutex);
}
}
void reader_function(void)
{
while (1)
{
pthread_mutex_lock(&mutex);
if (buffer_has_item==1)
{
buffer_has_item=0;
printf("Read once!/n");
}
pthread_mutex_unlock(&mutex);
}
}
</span>

這裡宣告瞭互斥鎖變數mutex,結構pthread_mutex_t為不公開的資料型別,其中包含一個系統分配的屬性物件。函式 pthread_mutex_init用來生成一個互斥鎖。NULL參數列明使用預設屬性。如果需要宣告特定屬性的互斥鎖,須呼叫函式 pthread_mutexattr_init。函式pthread_mutexattr_setpshared和函式 pthread_mutexattr_settype用來設定互斥鎖屬性。前一個函式設定屬性pshared,它有兩個取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用來不同程式中的執行緒同步,後者用於同步本程式的不同執行緒。在上面的例子中,我們使用的是預設屬性PTHREAD_PROCESS_ PRIVATE。後者用來設定互斥鎖型別,可選的型別有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、 PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它們分別定義了不同的上所、解鎖機制,一般情況下,選用最後一個預設屬性。
pthread_mutex_lock宣告開始用互斥鎖上鎖,此後的程式碼直至呼叫pthread_mutex_unlock為止,均被上鎖,即同一時間只能被一個執行緒呼叫執行。當一個執行緒執行到pthread_mutex_lock處時,如果該鎖此時被另一個執行緒使用,那此執行緒被阻塞,即程式將等待到另一個執行緒釋放此互斥鎖。在上面的例子中,我們使用了pthread_delay_np函式,讓執行緒睡眠一段時間,就是為了防止一個執行緒始終佔據此函式。
上面的例子非常簡單,就不再介紹了,需要提出的是在使用互斥鎖的過程中很有可能會出現死鎖:兩個執行緒試圖同時佔用兩個資源,並按不同的次序鎖定相應的互斥鎖,例如兩個執行緒都需要鎖定互斥鎖1和互斥鎖2,a執行緒先鎖定互斥鎖1,b執行緒先鎖定互斥鎖2,這時就出現了死鎖。此時我們可以使用函式 pthread_mutex_trylock,它是函式pthread_mutex_lock的非阻塞版本,當它發現死鎖不可避免時,它會返回相應的資訊,程式設計師可以針對死鎖做出相應的處理。另外不同的互斥鎖型別對死鎖的處理不一樣,但最主要的還是要程式設計師自己在程式設計注意這一點。
總結一下:
1) 只能用於"鎖"住臨界程式碼區域
2) 一個執行緒加的鎖必須由該執行緒解鎖.
鎖幾乎是我們學習同步時最開始接觸到的一個策略,也是最簡單, 最直白的策略.
4.3 條件變數
前一節中我們講述瞭如何使用互斥鎖來實現執行緒間資料的共享和通訊,互斥鎖一個明顯的缺點是它只有兩種狀態:鎖定和非鎖定。而條件變數通過允許執行緒阻塞和等待另一個執行緒傳送訊號的方法彌補了互斥鎖的不足,它常和互斥鎖一起使用。使用時,條件變數被用來阻塞一個執行緒,當條件不滿足時,執行緒往往解開相應的互斥鎖並等待條件發生變化。一旦其它的某個執行緒改變了條件變數,它將通知相應的條件變數喚醒一個或多個正被此條件變數阻塞的執行緒。這些執行緒將重新鎖定互斥鎖並重新測試條件是否滿足。一般說來,條件變數被用來進行執行緒間的同步。條件變數,與鎖不同, 條件變數用於等待某個條件被觸發
1) 大體使用的偽碼:
// 執行緒一程式碼
pthread_mutex_lock(&mutex);
// 設定條件為true
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
// 執行緒二程式碼
pthread_mutex_lock(&mutex);
while (條件為false)
pthread_cond_wait(&cond, &mutex);
修改該條件
pthread_mutex_unlock(&mutex);
需要注意幾點:
1)
第二段程式碼之所以在pthread_cond_wait外面包含一個while迴圈不停測試條件是否成立的原因是,
在 pthread_cond_wait被喚醒的時候可能該條件已經不成立,這個情況舉例:在pthread_cond_wait解鎖、測試到訊號後但是在加鎖前這個條件不成立了,那麼通過這個While還要再檢測這個條件是不是成立,那麼即使收到了這樣一個不穩定的錯誤訊號,while也是跳不出去的。 UNPV2對這個的描述是:"Notice that when
pthread_cond_wait returns, we always test the condition again, because
spurious wakeups can occur: a wakeup when the desired condition is
still not true.".
2)
pthread_cond_wait呼叫必須和某一個mutex一起呼叫, 這個mutex是在外部進行加鎖的mutex,
這個鎖的作用是互斥,因為兩個執行緒要對執行緒間共享的某個資料作操作,互斥就是必不可少的了。所以說pthread_cond_wait既進行了執行緒間的互斥還進行了執行緒間的同步。在呼叫pthread_cond_wait時, 內部的實現將首先將這個mutex解鎖, 然後等待條件變數被喚醒, 如果沒有被喚醒,
該執行緒將一直休眠, 也就是說, 該執行緒將一直阻塞在這個pthread_cond_wait呼叫中, 而當此執行緒被喚醒時,
將自動將這個mutex加鎖.
man文件中對這部分的說明是:
pthread_cond_wait
atomically unlocks the mutex (as per pthread_unlock_mutex) and waits
for the condition variable cond to be signaled. The thread execution
is suspended and does not consume any CPU time until the condition
variable is
signaled. The mutex must be locked by the calling thread
on entrance to pthread_cond_wait. Before returning to the calling
thread, pthread_cond_wait re-acquires mutex (as per pthread_lock_mutex).
也就是說pthread_cond_wait實際上可以看作是以下幾個動作的合體:
a.解鎖執行緒鎖
b.等待條件為true
c.加鎖執行緒鎖.

<span style="font-size:18px;">/*
* =====================================================================================
*
* Filename: pthread3.c
*
* Description: A program of showing semaphore
*
* Version: 1.0
* Created: 03/11/2009 10:03:23 PM
* Revision: none
* Compiler: gcc
*
* Author: Futuredaemon (BUPT), gnuhpc@gmail.com
* Company: BUPT_UNITED
*
* =====================================================================================
*/
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count = 0;
void * decrement_count(void *arg)
{
pthread_mutex_lock (&count_lock);
printf("decrement_count get count_lock/n");
while (count==0)
{
printf("decrement_count count == 0 /n");
printf("decrement_count before cond_wait /n");
pthread_cond_wait( &count_nonzero, &count_lock);
printf("decrement_count after cond_wait /n");
}
count = count -1;
pthread_mutex_unlock (&count_lock);
}
void * increment_count(void *arg)
{
pthread_mutex_lock(&count_lock);
printf("increment_count get count_lock/n");
if (count==0)
{
printf("increment_count before cond_signal/n");
pthread_cond_signal(&count_nonzero);
printf("increment_count after cond_signal/n");
}
count=count+1;
pthread_mutex_unlock(&count_lock);
}
int main(void)
{
pthread_t tid1,tid2;
pthread_mutex_init(&count_lock,NULL);
pthread_cond_init(&count_nonzero,NULL);
pthread_create(&tid1,NULL,decrement_count,NULL);
sleep(2);
pthread_create(&tid2,NULL,increment_count,NULL);
sleep(10);
pthread_exit(0);
}
</span>

我們現在要討論的是什麼時候單一Mutex不夠,還需要這麼麻煩用條件變數?
假設有共享的資源sum,與之相關聯的mutex是lock_s.假設每個執行緒對sum的操作很簡單的,與sum的狀態無關,比如只是sum++.那麼只用mutex足夠了.程式設計師只要確保每個執行緒操作前,取得lock,然後sum++,再unlock即可.
每個執行緒的程式碼將像這樣
add()
{
pthread_mutex_lock(lock_s);
sum++;
pthread_mutex_unlock(lock_s);
}
如果操作比較複雜,假設執行緒t0,t1,t2的操作是sum++,而執行緒t3則是在sum到達100的時候,列印出一條資訊,並對sum清零.這種情況下,
如果只用mutex,
則t3需要一個迴圈,每個迴圈裡先取得lock_s,然後檢查sum的狀態,如果sum>=100,則列印並清零,然後unlock.如果sum&
lt;100,則unlock,並sleep()本執行緒合適的一段時間.
這個時候,t0,t1,t2的程式碼不變,t3的程式碼如下
print()
{
while (1)
{
pthread_mutex_lock(lock_s);
if(sum<100)
{
printf(“sum reach 100!”);
pthread_mutex_unlock(lock_s);
}
else
{
pthread_mutex_unlock(lock_s);
my_thread_sleep(100);
return OK;
}
}
}
這種辦法有兩個問題
1) sum在大多數情況下不會到達100,那麼對t3的程式碼來說,大多數情況下,走的是else分支,只是lock和unlock,然後sleep().這浪費了CPU處理時間.
2) 為了節省CPU處理時間,t3會在探測到sum沒到達100的時候sleep()一段時間.這樣卻又帶來另外一個問題,亦即t3響應速度下降.可能在sum到達200的時候,t4才會醒過來.
3) 這樣,程式設計師在設定sleep()時間的時候陷入兩難境地,設定得太短了節省不了資源,太長了又降低響應速度.真是難辦啊!
這個時候,condition variable內褲外穿,從天而降,拯救了焦頭爛額的你.
你首先定義一個condition variable.
pthread_cond_t cond_sum_ready=PTHREAD_COND_INITIALIZER;
t0,t1,t2的程式碼只要後面加兩行,像這樣
add()
{
pthread_mutex_lock(lock_s);
sum++;
pthread_mutex_unlock(lock_s);
if(sum>=100)
pthread_cond_signal(&cond_sum_ready);
}
而t3的程式碼則是
print
{
pthread_mutex_lock(lock_s);
while(sum<100)
pthread_cond_wait(&cond_sum_ready, &lock_s);
printf(“sum is over 100!”);
sum=0;
pthread_mutex_unlock(lock_s);
return OK;
}
注意兩點:
1)
在thread_cond_wait()之前,必須先lock相關聯的mutex,
因為假如目標條件未滿足,pthread_cond_wait()實際上會unlock該mutex,
然後block,在目標條件滿足後再重新lock該mutex, 然後返回.
2)
為什麼是while(sum<100),而不是if(sum<100)
?這是因為在pthread_cond_signal()和pthread_cond_wait()返回之間,有時間差,假設在這個時間差內,還有另外一
個執行緒t4又把sum減少到100以下了,那麼t3在pthread_cond_wait()返回之後,顯然應該再檢查一遍sum的大小.這就是用
while的用意.
這麼一說就知道什麼時候要用條件變數了~就在涉及判斷共同變數狀態時,換句話說,也就是本節所說的要程式同步的時候用~
4.3訊號量
訊號量既可以作為二值計數器(即0,1),也可以作為資源計數器.
訊號量本質上是一個非負的整數計數器,它被用來控制對公共資源的訪問。當公共資源增加時,呼叫函式sem_post()增加訊號量。只有當訊號量值大於0時,才能使用公共資源,使用後,函式sem_wait()減少訊號量。函式sem_trywait()和函式pthread_ mutex_trylock()起同樣的作用,它是函式sem_wait()的非阻塞版本。下面我們逐個介紹和訊號量有關的一些函式,它們都在標頭檔案 /usr/include/semaphore.h中定義。
訊號量的資料型別為結構sem_t,它本質上是一個長整型的數。函式sem_init()用來初始化一個訊號量。它的原型為:
extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
sem為指向訊號量結構的一個指標;pshared不為0時此訊號量在程式間共享,否則只能為當前程式的所有執行緒共享;value給出了訊號量的初始值。
而函式int sem_getvalue(sem_t *sem, int *sval);則用於獲取訊號量當前的計數. 函式sem_destroy(sem_t *sem)用來釋放訊號量sem。
可以用訊號量模擬鎖和條件變數:
1) 鎖,在同一個執行緒內同時對某個訊號量先呼叫sem_wait再呼叫sem_post, 兩個函式呼叫其中的區域就是所要保護的臨界區程式碼了,這個時候其實訊號量是作為二值計數器來使用的.不過在此之前要初始化該訊號量計數為1,見下面例子中的程式碼.
2) 條件變數,在某個執行緒中呼叫sem_wait, 而在另一個執行緒中呼叫sem_post.
不過, 訊號量除了可以作為二值計數器用於模擬執行緒鎖和條件變數之外, 還有比它們更加強大的功能, 訊號量可以用做資源計數器, 也就是說初始化訊號量的值為某個資源當前可用的數量, 使用了一個之後遞減, 歸還了一個之後遞增。
訊號量與執行緒鎖,條件變數相比還有以下幾點不同:
1)鎖必須是同一個執行緒獲取以及釋放, 否則會死鎖.而條件變數和訊號量則不必.
2)訊號的遞增與減少會被系統自動記住, 系統內部有一個計數器實現訊號量,不必擔心會丟失, 而喚醒一個條件變數時,如果沒有相應的執行緒在等待該條件變數, 這次喚醒將被丟失.

<span style="font-size:18px;">/*
* =====================================================================================
*
* Filename: pthread4.c
*
* Description: A program of Semaphore
*
* Version: 1.0
* Created: 03/13/2009 11:54:35 PM
* Revision: none
* Compiler: gcc
*
* Author: Futuredaemon (BUPT), gnuhpc@gmail.com
* Company: BUPT_UNITED
*
* =====================================================================================
*/
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
#include <semaphore.h>
#define BUFSIZE 4
#define NUMBER 8
int sum_of_number=0;
/* 可讀 和 可寫資源數*/
sem_t write_res_number;
sem_t read_res_number;
/* 迴圈佇列 */
struct recycle_buffer{
int buffer[BUFSIZE];
int head,tail;
}re_buf;
/* 用於實現臨界區的互斥鎖,我們對其初始化*/
pthread_mutex_t buffer_mutex=PTHREAD_MUTEX_INITIALIZER;
static void *producer(void * arg)
{
int i;
for(i=0;i<=NUMBER;i++)
{
/* 減少可寫的資源數 */
sem_wait(&write_res_number);
/* 進入互斥區 */
pthread_mutex_lock(&buffer_mutex);
/*將資料複製到緩衝區的尾部*/
re_buf.buffer[re_buf.tail]=i;
re_buf.tail=(re_buf.tail+1)%BUFSIZE;
printf("procuder %d write %d./n",(int)pthread_self(),i);
/*離開互斥區*/
pthread_mutex_unlock(&buffer_mutex);
/*增加可讀資源數*/
sem_post(&read_res_number);
}
/* 執行緒終止,如果有執行緒等待它們結束,則把NULL作為等待其結果的返回值*/
return NULL;
}
static void * consumer(void * arg)
{
int i,num;
for(i=0;i<=NUMBER;i++)
{
/* 減少可讀資源數 */
sem_wait(&read_res_number);
/* 進入互斥區*/
pthread_mutex_lock(&buffer_mutex);
/* 從緩衝區的頭部獲取資料*/
num = re_buf.buffer[re_buf.head];
re_buf.head = (re_buf.head+1)%BUFSIZE;
printf("consumer %d read %d./n",pthread_self(),num);
/* 離開互斥區*/
pthread_mutex_unlock(&buffer_mutex);
sum_of_number+=num;
/* 增加客寫資源數*/
sem_post(&write_res_number);
} 
/* 執行緒終止,如果有執行緒等待它們結束,則把NULL作為等待其結果的返回值*/
return NULL;
}
int main(int argc,char ** argv)
{
/* 用於儲存執行緒的執行緒號 */
pthread_t p_tid;
pthread_t c_tid;
int i;
re_buf.head=0;
re_buf.tail=0;
for(i=0;i<BUFSIZE;i++)
re_buf.buffer[i] =0;
/* 初始化可寫資源數為迴圈佇列的單元數 */
sem_init(&write_res_number,0,BUFSIZE); // 這裡限定了可寫的bufsize,當寫執行緒寫滿buf時,會阻塞,等待讀執行緒讀取
/* 初始化可讀資源數為0 */
sem_init(&read_res_number,0,0);
/* 建立兩個執行緒,執行緒函式分別是 producer 和 consumer */
/* 這兩個執行緒將使用系統的預設的執行緒設定,如執行緒的堆疊大小、執行緒排程策略和相應的優先順序等等*/
pthread_create(&p_tid,NULL,producer,NULL);
pthread_create(&c_tid,NULL,consumer,NULL);
/*等待兩個執行緒完成退出*/
pthread_join(p_tid,NULL);
pthread_join(c_tid,NULL);
printf("The sum of number is %d/n",sum_of_number);
}
</span>

一直想整理一下Pthread,這次終於利用了幾個晚上,參考了多篇文件,整理了自己的思路,終於完成~

作者:gnuhpc
出處:
http://www.cnblogs.com/gnuhpc/

相關文章