C語言 之 多執行緒程式設計

tolele發表於2022-04-02

一、基礎知識

  • 計算機的核心是CPU,承擔了所有的計算任務。
  • 作業系統是計算機的管理者,負責任務的排程、資源的分配和管理,統領整個計算機硬體。
  • 應用程式則是具有某種功能的程式,程式是執行於作業系統之上的。

 

程式:

       程式是一個具有一定獨立功能的程式在一個資料集上的一次動態執行的過程,是作業系統進行資源分配和排程的一個獨立單位,是應用程式執行的載體。程式是程式的一次執行過程,是臨時的,有生命期的,是動態產生,動態消亡的。程式是一種抽象的概念,沒有統一的標準定義。

 

程式由程式、資料集合和程式控制塊三部分組成:

  • 程式:描述程式要完成的功能,是控制程式執行的指令集;
  • 資料集合:程式在執行時所需要的資料和工作區;
  • 程式控制塊:(Program Control Block,簡稱PCB),包含程式的描述資訊和控制資訊,是程式存在的唯一標誌。

 

執行緒:

執行緒的一些好處:(個人理解,保留質疑!)

        在程式為任務排程的最小單位時,但程式遇到堵塞時,作業系統會切換其它的程式進行處理。但由於程式不僅是排程的基本單位,同時還是資源分配的獨立單位,所以對程式進行切換時,開銷會比較大。為了減小切換時的開銷,將任務排程的最小單位這個責任交給了執行緒,程式依然是資源分配的單位。

 

執行緒的基本理解:

  • 是程式執行中一個單一的順序控制流程
  • 是程式執行流的最小單元
  • 是處理器排程和分派的基本單位

        一個程式可以有一個或多個執行緒,各個執行緒之間共享程式的記憶體空間(也就是所在程式的記憶體空間,不包括棧)。一個標準的執行緒由執行緒ID、當前指令指標(PC)、暫存器和堆疊組成。而程式由記憶體空間(程式碼、資料、程式空間、開啟的檔案)和一個或多個執行緒組成。

 

二、執行緒的建立

C語言中,使用pthread_create函式建立一個執行緒。該函式定義在標頭檔案pthread.h中,函式原型為:

int pthread_create(

    pthread_t *restrict tidp,

    const pthread_attr_t *restrict attr,

    void *(*start_rtn)(void *),

    void *restrict arg

  );

介紹:

  • 引數1:儲存執行緒ID,執行緒的控制程式碼,可通過該變數操縱指向的執行緒;
  • 引數2:執行緒的屬性,預設且一般是NULL;
  • 引數3:一個函式用於給新建立的執行緒去執行;
  • 引數4:引數3中的函式的傳入引數。不需要則為NULL
  • 返回值:成功返回0,失敗則返回錯誤編號;

 

另一個比較重要的函式:pthread_join()

  • 函式原型:int pthread_join(pthread_t thread,void**retval);
  • 功能:等待第一個引數的執行緒執行完成後,去執行retval指向的函式(起到執行緒同步的作用)

 

先開始我們C語言多執行緒程式設計的第一個小程式吧!

C語言 之 多執行緒程式設計
演示程式碼:

#include<stdio.h>

#include<stdlib.h>

#include<pthread.h>

 

void* Print(char* str)

{

printf("%s ",str);

}

 

int main()

{

pthread_t thread1,thread2;

pthread_create(&thread1,NULL,(void*)&Print,"Hello");

pthread_create(&thread2,NULL,(void*)&Print,"World");

return 0;

}
View Code

 

!在編譯時,pthread_create函式會報未定義引用的錯誤:

 

 

       在解決報錯後,得到了可執行檔案。但在執行時,卻看不到任何輸入。Why?這裡涉及到條件競爭的概念了,使用pthread_create函式建立了兩個執行緒,兩個執行緒建立後,並不影響主執行緒的執行,所以這裡就存在了三個執行緒的競爭關係了。可見,似乎主執行緒執行return 0;先於另外兩個執行緒的列印函式。主執行緒的退出會導致建立的執行緒退出,所以我們看不見它們的輸出。

 

 

那麼,為了使return 0語句慢點執行,可以採用sleep()函式進行延遲。

 

 

可以看到有列印了輸出,但有World HelloHello World兩種情況,也是因為競爭的原因。

 

三、執行緒同步與互斥鎖機制

在遇到條件競爭的問題中,上面採用sleep()函式進行延遲似乎也能解決問題。但實則不然,採用sleep()的弊端很是明顯:

  • 不能判斷延遲的時間長度,加上每次執行都會有所改變,更加不可控。
  • 會使程式執行卡頓,缺乏緊湊。

最適當的解決方法是採用鎖機制。

 

互斥鎖機制:

       通過訪問時對共享資源加鎖的方法,防止多個執行緒同時訪問共享資源。鎖有兩種狀態:未上鎖和已上鎖。在訪問共享資源時,進行上鎖,在訪問結束後,進行解鎖。若在訪問時,共享資源已被其它執行緒鎖住了,則進入堵塞狀態等待該執行緒釋放鎖再繼續下一步的執行。這種鎖我們稱為互斥鎖。

       通過鎖機制,前面的程式碼不難進行改變,這裡將不進行描述。下面將介紹一下生產者消費者模型,為了進一步演示鎖機制。

 

互斥鎖相關函式介紹:

1、pthread_mutex_init :初始化一個互斥鎖。

函式原型:int pthread_mutex_init(pthread_mutex_t*mutex,constpthread_mutexattr_t*attr);

 

2、pthread_mutex_lock:若所訪問的資源未上鎖,則進行lock,否則進入堵塞狀態。

函式原型:intpthread_mutex_lock(pthread_mutex_t*mutex);

 

3、pthread_mutex_unlock:對互斥鎖進行解鎖。

函式原型:intpthread_mutex_unlock(pthread_mutex_t*mutex);

 

4、pthread_mutex_destroy:銷燬一個互斥鎖。

函式原型:intpthread_mutex_destroy(pthread_mutex_t*mutex);

 

生產者消費者模型:

        生產者和消費者在同一時間段內共用同一個儲存空間,生產者往儲存空間中生成產品,消費者從儲存空間中取走產品。當儲存空間為空時,消費者阻塞;當儲存空間滿時,生產者阻塞。(下面程式碼中儲存空間為1

 

C語言 之 多執行緒程式設計
演示程式碼test02.c:

#include<stdio.h>

#include<pthread.h>

#include<stdlib.h>

 

int buf = 0;

pthread_mutex_t mut;

 

void producer()

{

while(1)

{

pthread_mutex_lock(&mut);

if(buf == 0)

{

buf = 1;

printf("produced an item.\n");

sleep(1);

}

pthread_mutex_unlock(&mut);

}

}

 

 

void consumer()

{

while(1)

{

pthread_mutex_lock(&mut);

if(buf == 1)

{

buf = 0;

printf("consumed an item.\n");

sleep(1);

}

pthread_mutex_unlock(&mut);

}

}

 

int main(void)

{

pthread_t thread1,thread2;

pthread_mutex_init(&mut,NULL);

pthread_create(&thread1,NULL,&producer,NULL);

consumer(&buf);

 

pthread_mutex_destroy(&mut);

return 0;

}
生產者消費者模型演示程式碼

 

執行結果:

        從執行結果可以看出,執行順序井然有序。生產後必是消費,消費完後必是生產。由於互斥鎖機制的存在,生產者和消費者不會同時對共享資源進行訪問。

 

四、訊號量機制

        上面瞭解到的互斥鎖有兩種狀態:資源為01的狀態。當我們所擁有的資源大於1時,可以採用訊號量機制。在訊號量機制中,我們有n個資源(n>0)。在訪問資源時,若n>=1,則可以訪問,同時訊號量-1,否則堵塞等待直到n>=1。其實互斥鎖可以看出訊號量的一種特殊情況(n=1)。

 

訊號量相關函式的介紹:

標頭檔案:semaphore.h

1sem_init函式:初始化一個訊號量。

函式原型:int sem_init(sem_t* sem, int pshared, unsigned int value);

引數:

  • sem:指定了要初始化的訊號量的地址;
  • pshared:如果其值為0,就表示訊號量是當前程式的區域性訊號量,否則訊號量就可以在多個程式間共享;
  • value:指定了訊號量的初始值;

返回值:成功=>0 , 失敗=> -1

 

2 sem_post函式:訊號量的值加1,如果加1後值大於0:等待訊號量的值變為大於0的程式或執行緒被喚醒。

函式原型:int sem_post(sem_t* sem);

返回值:成功=>0 , 失敗=> -1

 

3sem_wait函式:訊號量的減1操作。如果當前訊號量的值大於0,則可繼續執行。如果當前訊號量的值等於0,則會堵塞,直到訊號量的值大於0.

函式原型:int sem_wait(sem_t* sem);

返回值:成功=>0 , 失敗=> -1

 

4sem_destroy函式:銷燬一個訊號量。

函式原型:int sem_destroy(sem_t* sem);

返回值:成功=>0 , 失敗=> -1

 

5sem_getvalue函式:獲取訊號量中的值。

函式原型:int sem_getvalue(sem_t* sem, int* sval);

獲取訊號量的值,並放在&sval上。

C語言 之 多執行緒程式設計
#include<stdio.h>

#include<stdlib.h>

#include<pthread.h>

#include<semaphore.h>

#include<unistd.h>

sem_t npro; //還可以生產多少

sem_t ncon; //還可以消費多少

 

//producer function

void* producer(void* arg)

{

while(1)

{

int num;

sem_wait(&npro); //先判斷是否可以生產

sem_post(&ncon); //生產一個,可消費數+1

sem_getvalue(&ncon,&num);

printf("produce one,now have %d items.\n",num);

sleep(0.7);

}

 

}

 

//consumer function

void consumer(void* arg)

{

while(1)

{

int num;

sem_wait(&ncon); //判斷是否可以消費

sem_post(&npro); //消費一個,可生產數+1

sem_getvalue(&ncon,&num);

printf("consume one,now have %d items.\n",num);

sleep(1);

}

 

}

 

int main(void)

{

pthread_t thread1,thread2;

//init semaphore

sem_init(&npro,0,5); //設最大容量為5

sem_init(&ncon,0,0);

 

pthread_create(&thread1,NULL,&producer,NULL);

consumer(NULL);

return 0;

}
生產者消費者模型(訊號量機制)

 

執行結果:

 

 同樣也可以解決條件競爭問題,而且使用範圍更廣了。

五、小結

  • 程式和執行緒的基礎知識
  • 執行緒的建立以及執行緒存在的條件競爭問題

條件競爭的解決:

    • pthread_join()函式
    • 互斥鎖機制
    • 訊號量機制

以上內容若有不妥,麻煩提出(抱拳~)

 

參考文章:

tolele
2022-04-02

相關文章