天天看點

Linux的程序/線程間通信方式總結 01

Linux系統中的程序間通信方式主要以下幾種:

同一主機上的程序通信方式

   * UNIX程序間通信方式: 包括管道(PIPE), 有名管道(FIFO), 和信号(Signal)

   * System V程序通信方式:包括信号量(Semaphore), 消息隊列(Message Queue), 和共享記憶體(Shared Memory)

網絡主機間的程序通信方式

   * RPC: Remote Procedure Call 遠端過程調用

   * Socket: 目前最流行的網絡通信方式, 基于TCP/IP協定的通信方式.

各自的特點如下:

  • 管道(PIPE):管道是一種半雙工的通信方式,資料隻能單向流動,而且隻能在具有親緣關系(父子程序)的程序間使用。另外管道傳送的是無格式的位元組流,并且管道緩沖區的大小是有限的(管道緩沖區存在于記憶體中,在管道建立時,為緩沖區配置設定一個頁面大小)。
  • 有名管道 (FIFO): 有名管道也是半雙工的通信方式,但是它允許無親緣關系程序間的通信。
  • 信号(Signal): 信号是一種比較複雜的通信方式,用于通知接收程序某個事件已經發生。
  • 信号量(Semaphore):信号量是一個計數器,可以用來控制多個程序對共享資源的通路。它常作為一種鎖機制,防止某程序正在通路共享資源時,其他程序也通路該資源。是以,主要作為程序間以及同一程序内不同線程之間的同步手段。
  • 消息隊列(Message Queue):消息隊列是由消息的連結清單,存放在核心中并由消息隊列辨別符辨別。消息隊列克服了信号傳遞資訊少、管道隻能承載無格式位元組流以及緩沖區大小受限等缺點。
  • 共享記憶體(Shared Memory ):共享記憶體就是映射一段能被其他程序所通路的記憶體,這段共享記憶體由一個程序建立,但多個程序都可以通路。共享記憶體是最快的 IPC 方式,它是針對其他程序間通信方式運作效率低而專門設計的。它往往與其他通信機制,如信号量,配合使用,來實作程序間的同步和通信。
  • 套接字(Socket): 套解口也是一種程序間通信機制,與其他通信機制不同的是,它可用于不同主機間的程序通信。

線程間通信機制:

線程是一種輕量級的程序。 程序的通信機制主要包括 無名管道、有名管道、消息隊列、信号量、共享記憶體以及信号等。這些機制都是由linux核心來維護的,實作起來都比較複雜,而且占用大量的系統資源。 線程間的通信機制實作起來則相對簡單,主要包括 互斥鎖、條件變量、讀寫鎖和線程信号、信号量等。

互斥鎖通信機制: 1、 互斥鎖基本原理:互斥鎖以排他的方式防止資料被并發修改。當多個線程共享相同的記憶體時,需要確定每個線程看到的資料是一樣的。如果是隻讀,那麼一定是一樣的。如果是可讀可寫,在一個線程操作一個記憶體區域時,包含三個步驟,即讀出資料,修改資料,寫回資料。如果該線程在沒有寫回資料前,另一個線程來通路同一個區域,如果是讀,得不到最新的資料狀态,如果是寫,則會造成資料覆寫等問題。 互斥鎖就兩個狀态:開鎖(0),上鎖(1)。将某個共享資源和互斥鎖綁定後,對該共享資源的通路操作如下: A】在通路資源前,首先申請該互斥鎖,如果在開鎖狀态,則申請到該鎖對象,并立即占有該鎖(鎖定)。以防其他線程通路修改此資源。如果該鎖處于鎖定狀态,預設阻塞等待。 B】原則上隻有鎖定該互斥鎖的程序才能釋放此互斥鎖。但實際上,非鎖定的線程去解鎖也能成功。這個與鎖的條件有關,本文後續内容會詳細介紹。 互斥鎖基本操作函數如下: 功能 函數 初始化互斥鎖 pthread_mutex_init() 阻塞申請互斥鎖 pthread_mutex_lock() 釋放互斥鎖 pthread_mutex_unlock() 嘗試加鎖(非阻塞方式) pthread_mutex_trylock() 銷毀互斥鎖 pthread_mutex_destroy() 2、 互斥鎖的初始化和銷毀:pthread_mutex_init()、pthread_mutex_destroy() A】頭檔案:#include  <pthread.h> B】函數原型:extern int pthread_mutex_init(pthread_mutex_t  *__mutex,__const pthread_mutexattr_t  *__mutexattr); extern int pthread_mutex_destroy( pthread_mutex_t  *__mutex); C】傳回:操作成功傳回0,不成功則傳回非零值 D】參數: a、第一個參數為指向要初始化/銷毀的互斥鎖的指針。pthread_mutex_t即互斥量類型。在使用互斥鎖時,需在函數内定義一個這種類型的變量。其值可通過pthread_mutex_init()函數來以初始化,也可以通過使用pthread.h中定義的宏 PTHREAD_MUTEX_INITIALIZER  (隻對靜态配置設定的互斥量)來初始化。 如果是動态配置設定的互斥量,那麼釋放記憶體前需要用pthread_mutex_destroy,初始化用pthread_mutex_init()。 pthread.h中宏定義如下: #define   PTHREAD_MUTEX_INITIALIZER    { {0,} } 初始化方式如下: pthread_mutex_t   p = PTHREAD_MUTEX_INITIALIZER; b、第二個參數mutexattr是指向屬性對象的指針,該屬性對象定義要初始化鎖的屬性。如果該指針為NULL,則表示使用預設屬性。鎖的屬性在本文後續部分有詳細的介紹。 3、 互斥鎖的申請、釋放和嘗試解鎖:pthread_mutex_lock()、pthread_mutex_unlock()、pthread_mutex_trylock() A】函數原型:extern int  pthread_mutex_lock(pthread_mutex_t  *__mutex); extern  int  pthread_mutex_trylock(pthread_mutex_t  *__mutex); extern  int  pthread_mutex_unlock(pthread_mutex_t  *__mutex); B】傳回:成功傳回0,失敗傳回一個錯誤編号,以指明錯誤。(pthread_mutex_unlock()未設定errno變量)

條件變量通信機制: 1、條件變量基本原理:條件變量的出現,可以彌補互斥鎖的缺陷,有些問題僅僅靠互斥鎖無法解決。但是條件變量不能單獨使用,必須配合互斥鎖一起實作對資源的互斥通路。 例:互斥鎖無法解決的問題。 int  i = 3;int   j = 7; pthread A pthread_B pthread_mutex_lock(); pthread_mutex_lock() { { i++; if(i==j) j--; do_something(); } } pthread_mutex_unlock(); pthread_mutex_unlock();     —————————————————————————————————————— 上例中:兩個線程搶占互斥鎖,可能會導緻pthread B中的do_something()函數永遠無法執行的情況。這是程式員不想看到的。仔細分析後,可得到線程B其實不需要一直的申請釋放鎖,其運作僅僅需要一種情況而已。在A線程滿足i == j時,通知B線程執行do_something()即可。 條件變量基本操作: 功能 函數 初始化條件變量 pthread_cond_init() 阻塞等待條件變量 pthread_cond_wait() 通知等待該條件變量的第一個線程 pthread_cond_signal() 在指定的時間之内等待條件變量 pthread_cond_timedwait() 銷毀條件變量狀态 pthread_cond_destroy() 2、 條件變量的初始化和銷毀:pthread_cond_init()、pthread_cond_destroy() A】函數原型:extern  int  pthread_cond_init(pthread_cond_t  *__restrict  __cond,__const  pthread_condattr_t   *__restrict  __cond_attr); extern  int  pthread_cond_destroy( pthread_cond_t  *__cond); B】傳回:成功傳回0,失敗傳回錯誤編号以指明錯誤。 C】參數:第一個參數指向要初始化或損壞的條件變量的指針,條件變量的類型為pthread_cond_t。第二個參數指向條件屬性對象的指針。該屬性對象定義要初始化的條件變量的特性,如果此變量初始化為NULL,則為預設屬性。關于條件屬性,本文後續會有詳細介紹。 3、 通知等待條件變量的線程:pthread_cond_signal()、pthread_cond_broadcast() A】函數原型:extern  int  pthread_cond_signal ( pthread_cond_t  *__cond ); extern  int  pthread_cond_broadcast ( pthread_cond_t  *__cond ); B】說明: a、pthread_cond_signal()函數用于喚醒等待出現與條件變量cond相關聯的條件的第一個線程。如果cond上沒有阻塞任何線程,則此函數不起作用。如果cond阻塞了多個線程,則排程政策将确定要取消阻塞的線程。顯然,在此函數中,隐含的釋放了目前線程占用的信号量(備注:信号和信号量不是一個東西,在程序和程序通信中會詳細說明信号和信号量)。 b、pthread_cond_broadcast()函數用于喚醒等待出現與條件變量cond關聯的條件的所有線程。如果cond上沒有阻塞任何線程,則此函數不起作用。 4、 等待條件變量:pthread_cond_wait()、pthread_cond_timedwait() A】函數原型:extern  int pthread_cond_wait(pthread_cond_t  *__restrict  __cond,pthread_mutex_t  *__restrict  __mutex); extern  int  pthread_cond_timedwait( pthread_cond_t  *__restrict  __cond,pthread_mutex_t  *__restrict  __mutex , __const  struct  timespec  *__restrict  __abstime); B】參數說明:cond是指向要等待的條件變量的指針,mutex指向與條件變量cond關聯的互斥鎖的指針。pthread_cond_wait()、 pthread_cond_timedwait()函數的實作是一個 先對互斥鎖進行解鎖,再加鎖的一個過程。pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)函數傳入的參數mutex用于保護條件,因為我們在調用pthread_cond_wait時,如果條件不成立我們就進入阻塞,但是進入阻塞這個期間,如果條件變量改變了的話,那我們就漏掉了這個條件。因為這個線程還沒有放到等待隊列上,是以調用pthread_cond_wait前要先鎖互斥量,即調用pthread_mutex_lock(),pthread_cond_wait在把線程放進阻塞隊列後,自動對mutex進行解鎖,使得其它線程可以獲得加鎖的權利。這樣其它線程才能對臨界資源進行通路并在适當的時候喚醒這個阻塞的程序。當pthread_cond_wait傳回的時候又自動給mutex加鎖。 Thread A:當滿足條件的時候發送一個信号。 Thread B:先給一個mutex加鎖,以便互斥通路count的值。在一個while循環裡等待count值達到MAX_COUNT。因為當某個條件滿足時,可能會有多個線程被喚醒。是以需要判斷條件是否還滿足。pthread_cond_wait首先把調用線程放入條件變量的等待隊列,然後再釋放mutex。當函數傳回時,mutex又會被加上鎖。最後對mutex解鎖,讓其他線程使用count變量。(加了寫鎖的等待就是占着茅坑不拉屎,有資料更新此操作域又執行不了寫操作,隻能先解鎖咯~~~~)

pthread_cond_timedwait()多了一個參數,即abstime,abstime是從1970年1月1日00:00:00以來的秒數,是一個絕對時間。等待時間到,則不阻塞,往下執行程式。timespec結構體聲明如下: struct  timespec { long  tv_sec; long  tv_nsec; }; C】傳回:如果成功,傳回0,失敗則傳回一個錯誤編号。

執行個體:

#include<stdlib.h>
#include<stdio.h>  
#include<pthread.h>
#include<errno.h>
#include<unistd.h>

typedef void* (*fun)(void*);

int x=1,y=4;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void* thread1(void*);
void* thread2(void*);

int main(int argc, char** argv)
{
  printf("enter main!\n");
  pthread_t tid1, tid2;
  int rc1=0, rc2=0;
  rc2 = pthread_create(&tid2, NULL, thread2, NULL);
  if(rc2 != 0)
    printf("%s: %d\n",__func__, strerror(rc2));

  rc1 = pthread_create(&tid1, NULL, thread1, &tid2);
  if(rc1 != 0)
    printf("%s: %d\n",__func__, strerror(rc1));
  sleep(1);
  printf("leave main!\n");
  exit(0);	
}

void* thread1(void* arg)
{
  printf("enter thread1\n");
  printf("this is thread1: x= %d,y=%d. thread id is %u\n",x,y, (unsigned int)pthread_self());
  pthread_mutex_lock(&mutex);
  x+=y;
  if(x>y)
    pthread_cond_signal(&cond);
   
  printf("this is thread1: x= %d,y=%d. thread id is %u\n", x,y, (unsigned int)pthread_self());
  pthread_mutex_unlock(&mutex);
  pthread_join(*(pthread_t*)arg, NULL);
  printf("leave thread1\n");
  pthread_exit(0);
}

void* thread2(void* arg)
{
  printf("enter thread2\n");
  printf("this is thread2: x= %d,y=%d. thread id is %u\n", x,y, (unsigned int)pthread_self());
  pthread_mutex_lock(&mutex);
  while(x<=y)
    pthread_cond_wait(&cond, &mutex);
  x-=y;
  printf("this is thread2: x= %d,y=%d. thread id is %u\n", x,y, (unsigned int)pthread_self());
  pthread_mutex_unlock(&mutex);
  printf("leave thread2\n");
  pthread_exit(0);
}
           

有的時候僅僅依靠鎖住共享資源來使用它是不夠的。有時候共享資源隻有某些狀态的時候才能夠使用。比方說,某個線程如果要從堆棧中讀取資料,那麼如果棧中沒有資料就必須等待資料被壓棧。這種情況下的同步使用互斥鎖

是不夠的。另一種同步的方式--條件變量,就可以使用在這種情況下。條件變量(Condition Variable)是線程間的一種同步機制,提供給兩個線程協同完成任務的一種方法,使用條件變量可以以原子方式阻塞線程,直到某個特定條件為真為止。條件變量的測試一般是用互斥量來保護的,用來確定每一時刻隻有一個線程能夠改變條件變量,如果條件為假,線程通常會基于條件變量而阻塞,并以原子方式釋放等待條件變化的互斥鎖。如果另一個線程更改了條件,該線程可能會向相關的條件變量發出信号,進而使一個或多個等待的線程執行以下操作:

  • 喚醒
  • 再次擷取互斥鎖
  • 重新評估條件

在以下情況下,條件變量可用于在程序之間同步線程:

  • 線程是在可以寫入的記憶體中配置設定的
  • 記憶體由協作程序共享

Condition Variable用pthread_cond_t類型的變量表示,和Mutex的初始化和銷毀類似,pthread_cond_init函數初始化一個Condition Variable,attr參數為NULL則表示預設屬性,pthread_cond_destroy函數銷毀一個Condition Variable。如果ConditionVariable是靜态配置設定的,也可以用宏定義PTHEAD_COND_INITIALIZER初始化,相當于用pthread_cond_init函數初始化并且attr參數為NULL。

條件變量的相關函數如下:

#include <pthread.h>
 
int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr); 
int pthread_cond_signal(pthread_cond_t *cptr); 
//Both return: 0 if OK, positive Exxx value on error      

pthread_cond_wait 用于等待某個特定的條件為真,一個線程可以調用pthread_cond_wait在一個Condition Variable上阻塞等待,這個函數做以下三步操作:

1. 釋放Mutex

2. 阻塞等待

3. 當被喚醒時,重新獲得Mutex并傳回

注意:3個操作是原子性的操作,之是以一開始要釋放Mutex,是因為需要讓其他線程進入臨界區去更改條件,或者也有其他線程需要進入臨界區等待條件。

pthread_cond_signal 用于通知阻塞的線程某個特定的條件為真了。在調用者兩個函數之前需要聲明一個 pthread_cond_t 類型的變量,用于這兩個函數的參數。

為什麼條件變量始終與互斥鎖一起使用,對條件的測試是在互斥鎖(互斥)的保護下進行的呢?因為“某個特性條件”通常是在多個線程之間共享的某個變量。互斥鎖允許這個變量可以在不同的線程中設定和檢測。

通常, pthread_cond_wait 隻是喚醒等待某個條件變量的一個線程。如果需要喚醒所有等待某個條件變量的線程,需要調用:

int pthread_cond_broadcast (pthread_cond_t * cptr);      

預設情況下面,阻塞的線程會一直等待,知道某個條件變量為真。如果想設定最大的阻塞時間可以調用:

int pthread_cond_timedwait (pthread_cond_t * cptr, pthread_mutex_t *mptr, const struct timespec *abstime);      

如果時間到了,條件變量還沒有為真,仍然傳回,傳回值為ETIME。

二、條件變量使用規範

(一)、等待條件代碼

pthread_mutex_lock(&mutex);

while (條件為假)

pthread_cond_wait(cond, mutex);

修改條件

pthread_mutex_unlock(&mutex);

(二)、給條件發送通知代碼

pthread_mutex_lock(&mutex);

設定條件為真

pthread_cond_signal(cond);

pthread_mutex_unlock(&mutex);

至于為什麼在被喚醒之後還要再次進行條件判斷(即為什麼要使用while循環來判斷條件),是因為可能有“驚群效應”。有人覺得此處既然是被喚醒的,肯定是滿足條件了,其實不然。如果是多個線程都在等待這個條件,而同時隻能有一個線程進行處理,此時就必須要再次條件判斷,以使隻有一個線程進入臨界區處理。考慮以下情況:

      1,pthread_cond_signal在多處理器上可能同時喚醒多個線程,當你隻能讓一個線程處理某個任務時,其它被喚醒的線程就需要繼續 wait,while循環的意義就展現在這裡了,而且規範要求pthread_cond_signal至少喚醒一個pthread_cond_wait上的線程,其實有些實作為了簡單在單處理器上也會喚醒多個線程. 

       2,某些應用,如線程池,pthread_cond_broadcast喚醒全部線程,但我們通常隻需要一部分線程去做執行任務,是以其它的線程需要繼續wait.是以強烈推薦此處使用while循環.

       其實說白了很簡單,就是pthread_cond_signal()也可能喚醒多個線程,而如果你同時隻允許一個線程通路的話,就必須要使用while來進行條件判斷,以保證臨界區内隻有一個線程在處理。