歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> Linux技術

Linux下的多線程編程

作者:gnuhpc
出處:http://www.cnblogs.com/gnuhpc/
原文鏈接:/content/3668180.html
本文作者: 姚繼鋒 (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的數據更有可能給多線程程序帶來災難性的打擊,這些正是編寫多線程程序時最需要注意的地方。
除了以上所說的優點外,不和進程比較,多線程程序作為一種多任務、並發的工作方式,當然有以下的優點:
提高應用程序響應。這對圖形界面的程序尤其有意義,當一個操作耗時很長時,整個系統都會等待這個操作,此時程序不會響應鍵盤、鼠標、菜單的操作,而使用多線程技術,將耗時長的操作(time consuming)置於一個新的線程,可以避免這種尴尬的情況。
使多CPU系統更加有效。操作系統會保證當線程數不大於CPU數目時,不同的線程運行於不同的CPU上。
改善程序結構。一個既長又復雜的進程可以考慮分為多個線程,成為幾個獨立或半獨立的運行部分,這樣的程序會利於理解和修改。
  下面我們先來嘗試編寫一個簡單的多線程程序。

2 簡單的多線程編程

  Linux系統下的多線程遵循POSIX線程接口,稱為pthread。編寫Linux下的多線程程序,需要使用頭文件pthread.h,連接時需要使用庫libpthread.a。順便說一下,Linux下pthread的實現是通過系統調用clone()來實現的。clone()是Linux所特有的系統調用,它的使用方式類似fork,關於clone()的詳細情況,有興趣的讀者可以去查看有關文檔說明。下面我們展示一個最簡單的多線程程序
example1.c。

/*
* =====================================================================================
*
* 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), [email protected]
* 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;
}


  上面的示例中,我們使用到了兩個函數, 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, ¶m); 
param.sched_priority=newprio; 
pthread_attr_setschedparam(&attr, ¶m); 
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 互斥鎖

互斥鎖用來保證一段時間內只有一個線程在執行一段代碼。必要性顯而易見:假設各個線程向同一個文件順序寫入數據,最後得到的結果一定是災難性的。
我們先看下面一段代碼。這是一個讀/寫程序,它們公用一個緩沖區,並且我們假定一個緩沖區只能保存一條信息。即緩沖區只有兩個狀態:有信息或沒有信息。

/*
* =====================================================================================
*
* 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), [email protected]
* 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);
  }
}


  這裡聲明了互斥鎖變量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 thread_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.加鎖線程鎖.

/*
* =====================================================================================
*
* 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), [email protected]
* 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);
}


  我們現在要討論的是什麼時候單一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>=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。
可以用信號量模擬鎖和條件變量:
鎖,在同一個線程內同時對某個信號量先調用sem_wait再調用sem_post, 兩個函數調用其中的區域就是所要保護的臨界區代碼了,這個時候其實信號量是作為二值計數器來使用的.不過在此之前要初始化該信號量計數為1,見下面例子中的代碼.
條件變量,在某個線程中調用sem_wait, 而在另一個線程中調用sem_post.
  不過, 信號量除了可以作為二值計數器用於模擬線程鎖和條件變量之外, 還有比它們更加強大的功能, 信號量可以用做資源計數器, 也就是說初始化信號量的值為某個資源當前可用的數量, 使用了一個之後遞減, 歸還了一個之後遞增。
  信號量與線程鎖,條件變量相比還有以下幾點不同:
鎖必須是同一個線程獲取以及釋放, 否則會死鎖.而條件變量和信號量則不必.
信號的遞增與減少會被系統自動記住, 系統內部有一個計數器實現信號量,不必擔心會丟失, 而喚醒一個條件變量時,如果沒有相應的線程在等待該條件變量, 這次喚醒將被丟失.

/*
* =====================================================================================
*
* 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), [email protected]
* 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);
}


  一直想整理一下Pthread,這次終於利用了幾個晚上,參考了多篇文檔,整理了自己的思路,終於完成~
寫者:zengzy
出處:' target='_blank'>http://www.cnblogs.com/zengzy
標題有【轉】字樣的文章從別的地方轉過來的,否則為個人學習筆記
Copyright © Linux教程網 All Rights Reserved