1. 程式人生 > >C語言多執行緒例項之pthread的應用(在windows下的應用(win7))

C語言多執行緒例項之pthread的應用(在windows下的應用(win7))

Pthread是由POSIX提出的一套通用的執行緒庫,在linux平臺下,它被廣泛的支援,而windows平臺下,卻並不被支援,而pthreads-w32為我們提供瞭解決方案,本文我們準備在我們的windows平臺下進行pthread-w32的安裝,在網路上有類似的文章,但是講的都是比較老的平臺,在windows8下支援並不全面,不過可以作為參考。我們在這裡貼出幾個網址,供參考使用。

   Windows 7 64bit和Visual Studio 2010下安裝及使用Pthread-w32 2.8 

   windows下使用pthread庫(轉)

    如果你的是XP系統或者win7 32位系統,那麼,那兩篇文章已經足以你完成pthread-w32的安裝了。現在,我們開始講我們的嘗試過程。

一、安裝平臺

    windows8 64位系統,Microsoft Visual Studio 2012

二、pthreads-w32 下載地址

    我們這裡下載最新版本pthreads-w32-2-9-1

下載後解壓,可以看到共有三個資料夾

我們用到的主要是“Pre-built.2”這個資料夾下的三個資料夾,分別是動態連結庫、標頭檔案、靜態連結庫

三、配置標頭檔案及靜態連結庫

    這裡有多種方式,我們這裡只提到我們用到的一種,總之目的是讓我們建立的工程能夠找到對應的標頭檔案、靜態庫檔案,以及執行時程式能夠找到動態連結庫檔案。

這裡,我們直接把標頭檔案拷貝到Visual Studio的預設路徑的標頭檔案中,即把include資料夾中的三個檔案直接拷貝到Visual Studio安裝目錄下VC->include資料夾下,例如我將include中檔案拷貝到的位置是

E:\Program Files\Microsoft Visual Studio 11.0\VC\include

這樣,我們就不必每次在專案用到時都配置一遍,特別是在Visual Studio2012 貌似不支援全域性的標頭檔案配置時(不確定,如果誰找到了可以告訴我一聲),這種方式對於經常會建一些小專案的人來說,相對節省時間。

同樣的辦法與原因,我們也可以把lib資料夾下的內容拷貝到Visual Studio安裝目錄下預設的lib尋找路徑中,即VC->lib中,例如我將lib資料夾下的x64與x86兩個檔案直接拷貝到

    E:\Program Files\Microsoft Visual Studio 11.0\VC\lib

的下面。

四、配置動態連結庫

   和標頭檔案和靜態連結庫的配置方式相似,我們這裡將dll資料夾的內容放到我們程式能夠找到的位置,我們的方案是

把dll下的x64資料夾下的兩個檔案,即pthreadGC2.dll與pthreadVC2.dll拷貝到C:\Windows\System32下(用於64位程式的執行)

把dll下的x86資料夾下的五個檔案,拷貝到C:\Windows\SysWOW64下(用於32位程式的執行),注意一下,千萬不能將這些檔案拷貝反位置,否則,程式執行時會提示說找不到對應的dll檔案。這些在網上的很多文章中都被忽略掉了,所以我們特別提出。

五:注意事項:

1)在linux下編譯的時候要加上-lpthread ,如:

gcc -g thread_test.c -o thread_test -lpthread

2)按上述步驟如果出錯,則把lib資料夾下x86中的檔案直接拷貝到上一級目錄。我的問題解決了。

六:列兩個多執行緒的例項:

/*mutex.c*/
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
/*全域性變數*/
int gnum = 0;
/*互斥量 */
pthread_mutex_t mutex;
/*宣告執行緒執行服務程式*/
static void pthread_func_1 (void);
static void pthread_func_2 (void);

int main (void)
{
 /*執行緒的識別符號*/
  pthread_t pt_1 = 0;
  pthread_t pt_2 = 0;
  int ret = 0;
  /*互斥初始化*/
  pthread_mutex_init (&mutex, NULL);
  /*分別建立執行緒1、2*/
  ret = pthread_create( &pt_1,                  //執行緒識別符號指標
                                     NULL,                  //預設屬性
                                     (void *)pthread_func_1,//執行函式
                                     NULL);                  //無引數
  if (ret != 0)
  {
     perror ("pthread_1_create");
  }

  ret = pthread_create( &pt_2,                  //執行緒識別符號指標
                                     NULL,                   //預設屬性
                                     (void *)pthread_func_2, //執行函式
                                     NULL);                  //無引數
  if (ret != 0)
  {
     perror ("pthread_2_create");
  }
  /*等待執行緒1、2的結束*/
  pthread_join (pt_1, NULL);
  pthread_join (pt_2, NULL);

  printf ("main programme exit!/n");
  return 0;
}
/*執行緒1的服務程式*/
static void pthread_func_1 (void)
{
  int i = 0;

  for( i=0; i<3; i++ ){
    printf ("This is pthread_1!/n");
    pthread_mutex_lock(&mutex); /*獲取互斥鎖*/
        /*注意,這裡以防執行緒的搶佔,以造成一個執行緒在另一個執行緒sleep時多次訪問互斥資源,所以sleep要在得到互斥鎖後呼叫*/
    sleep (1);
    /*臨界資源*/
    gnum++;
    printf ("Thread_1 add one to num:%d/n",gnum);
    pthread_mutex_unlock(&mutex); /*釋放互斥鎖*/
  }

  pthread_exit ( NULL );
}
/*執行緒2的服務程式*/
static void pthread_func_2 (void)
{
  int i = 0;

  for( i=0; i<5; i++ )  {
    printf ("This is pthread_2!/n");
    pthread_mutex_lock(&mutex); /*獲取互斥鎖*/
        /*注意,這裡以防執行緒的搶佔,以造成一個執行緒在另一個執行緒sleep時多次訪問互斥資源,所以sleep要在得到互斥鎖後呼叫*/
    sleep (1);
    /*臨界資源*/
    gnum++;
    printf ("Thread_2 add one to num:%d/n",gnum);
    pthread_mutex_unlock(&mutex); /*釋放互斥鎖*/

  }

  pthread_exit ( NULL );
} 

有兩類函式在這裡說明一下:

第一類是互斥鎖:

linux下為了多執行緒同步,通常用到鎖的概念。
posix下抽象了一個鎖型別的結構:ptread_mutex_t。通過對該結構的操作,來判斷資源是否可以訪問。顧名思義,加鎖(lock)後,別人就無法開啟,只有當鎖沒有關閉(unlock)的時候才能訪問資源。

即物件互斥鎖的概念,來保證共享資料操作的完整性。每個物件都對應於一個可稱為” 互斥鎖” 的標記,這個標記用來保證在任一時刻,只能有一個執行緒訪問該物件。

使用互斥鎖(互斥)可以使執行緒按順序執行。通常,互斥鎖通過確保一次只有一個執行緒執行程式碼的臨界段來同步多個執行緒。互斥鎖還可以保護單執行緒程式碼。

  要更改預設的互斥鎖屬性,可以對屬性物件進行宣告和初始化。通常,互斥鎖屬性會設定在應用程式開頭的某個位置,以便可以快速查詢和輕鬆修改。

1.函式原型:

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);

該函式用於C函式的多執行緒程式設計中,互斥鎖的初始化。

  pthread_mutex_init()函式是以動態方式建立互斥鎖的,引數attr指定了新建互斥鎖的屬性。如果引數attr為NULL,則使用預設的互斥鎖屬性,預設屬性為快速互斥鎖 。互斥鎖的屬性在建立鎖的時候指定,在LinuxThreads實現中僅有一個鎖型別屬性,不同的鎖型別在試圖對一個已經被鎖定的互斥鎖加鎖時表現不同。

  pthread_mutexattr_init()函式成功完成之後會返回零,其他任何返回值都表示出現了錯誤。

  函式成功執行後,互斥鎖被初始化為鎖住態。

2. 互斥鎖屬性

互斥鎖的屬性在建立鎖的時候指定,在LinuxThreads實現中僅有一個鎖型別屬性,不同的鎖型別在試圖對一個已經被鎖定的互斥鎖加鎖時表現不同。當前(glibc2.2.3,linuxthreads0.9)有四個值可供選擇:

  * PTHREAD_MUTEX_TIMED_NP,這是預設值,也就是普通鎖。當一個執行緒加鎖以後,其餘請求鎖的執行緒將形成一個等待佇列,並在解鎖後按優先順序獲得鎖。這種鎖策略保證了資源分配的公平性。

  * PTHREAD_MUTEX_RECURSIVE_NP,巢狀鎖,允許同一個執行緒對同一個鎖成功獲得多次,並通過多次unlock解鎖。如果是不同執行緒請求,則在加鎖執行緒解鎖時重新競爭。

  * PTHREAD_MUTEX_ERRORCHECK_NP,檢錯鎖,如果同一個執行緒請求同一個鎖,則返回EDEADLK,否則與PTHREAD_MUTEX_TIMED_NP型別動作相同。這樣就保證當不允許多次加鎖時不會出現最簡單情況下的死鎖。

  * PTHREAD_MUTEX_ADAPTIVE_NP,適應鎖,動作最簡單的鎖型別,僅等待解鎖後重新競爭。

3. 其他鎖操作

  鎖操作主要包括加鎖pthread_mutex_lock()、解鎖pthread_mutex_unlock()和測試加鎖 pthread_mutex_trylock()三個,不論哪種型別的鎖,都不可能被兩個不同的執行緒同時得到,而必須等待解鎖。對於普通鎖和適應鎖型別,解鎖者可以是同進程內任何執行緒;而檢錯鎖則必須由加鎖者解鎖才有效,否則返回EPERM;對於巢狀鎖,文件和實現要求必須由加鎖者解鎖,但實驗結果表明並沒有這種限制,這個不同目前還沒有得到解釋。在同一程序中的執行緒,如果加鎖後沒有解鎖,則任何其他執行緒都無法再獲得鎖。

   int pthread_mutex_lock(pthread_mutex_t *mutex)

  int pthread_mutex_unlock(pthread_mutex_t *mutex)

  int pthread_mutex_trylock(pthread_mutex_t *mutex)

  pthread_mutex_trylock()語義與pthread_mutex_lock()類似,不同的是在鎖已經被佔據時返回EBUSY而不是掛起等待。

  1. 死鎖:

      死鎖主要發生在有多個依賴鎖存在時, 會在一個執行緒試圖以與另一個執行緒相反順序鎖住互斥量時發生. 如何避免死鎖是使用互斥量應該格外注意的東西。

      總體來講, 有幾個不成文的基本原則:

      對共享資源操作前一定要獲得鎖。

      完成操作以後一定要釋放鎖。

      儘量短時間地佔用鎖。

      如果有多鎖, 如獲得順序是ABC連環扣, 釋放順序也應該是ABC。

      執行緒錯誤返回時應該釋放它所獲得的鎖。

第二類是多執行緒:

pthread_create函式

原型:int  pthread_create((pthread_t  *thread, 
                             pthread_attr_t  *attr, 
                             void  *(*start_routine)(void  *), 
                             void  *arg)

用法:#include  <pthread.h>

功能:建立執行緒(實際上就是確定呼叫該執行緒函式的入口點),線上程建立以後,就開始執行相關的執行緒函式。

說明:thread:執行緒識別符號;

          attr:執行緒屬性設定;

          start_routine:執行緒函式的起始地址;

          arg:傳遞給start_routine的引數;

          返回值:成功,返回0;出錯,返回-1。

pthread_join函式

函式原型:int pthread_join(pthread_t tid, void **status);


功能:pthread_join()函式會一直阻塞呼叫執行緒,直到指定的執行緒tid終止。當pthread_join()返回之後,應用程式可回收

與已終止執行緒關聯的任何資料儲存空間,(另外也可設定執行緒attr屬性,當執行緒結束時直接回收資源)如果沒有必要等待特定的執行緒

終止之後才進行其他處理,則應當將該執行緒分離pthread_detach()。


標頭檔案:#include <pthread.h>

pthread非linux系統的預設庫, 需手動連結-執行緒庫 -lpthread


引數:

tid:需要等待的執行緒,指定的執行緒必須位於當前的程序中,而且不得是分離執行緒

status:執行緒tid所執行的函式start_routine的返回值(start_routine返回值地址需要保證有效),其中status可以為nullptr


返回值:

呼叫成功完成後,pthrea_join()返回0.其他任何返回值都表示出現了錯誤。如果檢測到以下任意情況

pthread_join()將失敗並返回相應的值

ESRCH
描述: 沒有找到與給定的執行緒ID 相對應的執行緒。(如果多個執行緒等待同一個執行緒終止,則所有等待執行緒將一直等到目標執行緒終止。

然後一個等待執行緒成功返回。其餘的等待執行緒將失敗返回ESRCH錯誤)
EDEADLK
描述: 將出現死鎖,如一個執行緒等待其本身,或者執行緒A和執行緒B 互相等待。
EINVAL
描述: 與給定的執行緒ID 相對應的執行緒是分離執行緒。

pthread_exit函式

  原型:void  pthread_exit(void  *retval)

用法:#include  <pthread.h>

功能:使用函式pthread_exit退出執行緒,這是執行緒的主動行為;由於一個程序中的多個執行緒是共享資料段的,因此通常線上程退出之後,退出執行緒所佔用的資源並不會隨著執行緒的終止而得到釋放,但是可以用pthread_join()函式(下篇部落格中講到)來同步並釋放資源。

說明:retval:pthread_exit()呼叫執行緒的返回值,可由其他函式如pthread_join來檢索獲取。

舉例:

/*thread.c*/  
#include <stdio.h>  
#include <pthread.h>  

/*執行緒一*/  
void thread_1(void)  
{  
    int i=0;  
    for(i=0;i<=6;i++)  
    {  
        printf("This is a pthread_1.\n");  
        if(i==2)  
            pthread_exit(0);                      //用pthread_exit()來呼叫執行緒的返回值,用來退出執行緒,但是退出執行緒所佔用的資源不會隨著執行緒的終止而得到釋放  
        sleep(1);  
    }  
}  

/*執行緒二*/  
void thread_2(void)  
{  
    int i;  
    for(i=0;i<3;i++)  
        printf("This is a pthread_2.\n");           
    pthread_exit(0);                              //用pthread_exit()來呼叫執行緒的返回值,用來退出執行緒,但是退出執行緒所佔用的資源不會隨著執行緒的終止而得到釋放  
}  

int main(void)  
{  
    pthread_t id_1,id_2;  
    int i,ret;  
/*建立執行緒一*/  
    ret=pthread_create(&id_1,NULL,(void  *) thread_1,NULL);  
    if(ret!=0)  
    {  
        printf("Create pthread error!\n");  
    return -1;  
    }  
/*建立執行緒二*/  
     ret=pthread_create(&id_2,NULL,(void  *) thread_2,NULL);  
    if(ret!=0)  
    {  
        printf("Create pthread error!\n");  
    return -1;  
    }  
/*等待執行緒結束*/  
    pthread_join(id_1,NULL);  
    pthread_join(id_2,NULL);  
    return 0;  
}  

pthread_create 傳入引數:

pthread_create如何傳遞兩個引數以上的引數
涉及多引數傳遞給執行緒的,都需要使用結構體將引數封裝後,將結構體指標傳給執行緒
定義一個結構體
struct mypara
{
var para1;//引數1
var para2;//引數2
}
將這個結構體指標,作為void *形參的實際引數傳遞
struct mypara pstru;
pthread_create(&ntid, NULL, thr_fn,& (pstru));
函式中需要定義一個mypara型別的結構指標來引用這個引數

void *thr_fn(void *arg)
{
mypara *pstru;
pstru = (struct mypara *) arg;
pstru->para1;//引數1
pstru->para2;//引數2
}

pthread_create函式接受的引數只有一個void
*型的指標,這就意味著你只能通過結構體封裝超過一個以上的引數作為一個整體傳遞。這是pthread_create函式的介面限定的,別人已經明確表明
我只接受一個引數,你硬要塞給他兩個肯定會出錯了。所以通過結構體這種組合結構變通一下,同樣實現了只通過一個引數傳遞,但通過結構指標對結構資料成員的
引用實現多引數的傳遞
這種用結構體封裝多引數的用法不僅僅用在pthread_create函式中,如果你自己設計的函式需要的引數很多〉=5個以上,都可以考慮使用結構體封
裝,這樣對外你的介面很簡潔清晰,你的函式的消費者使用起來也很方便,只需要對結構體各個成員賦值即可,避免了引數很多時漏傳、誤傳(引數串位)的問題
結構體內包含結構體完全沒有問題,很多應用都這麼使用

舉例:

static thread_func(void *arg)
{
    THREAD_CREATE_ARG *pMypara;
    pMypara = (THREAD_CREATE_ARG *)arg;

    //這裡可以放執行緒要實現的功能;
        //e.g  func(pMypara->para1, pMypara->para2,pMypara->para3, ...);
}

pthread_t thread_id;

struct thread_create_arg THREAD_CREATE_ARG
{
    var para1;
    var para2;
    var para3;
    ....
}   

THREAD_CREATE_ARG thread_create_var;

pthread_create(&thread_id, NULL, (void *)thread_func, &thread_create_var);