C語言 之 多線程編程


一、基礎知識

  • 計算機的核心是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語言多線程編程的第一個小程序吧!

演示代碼:

#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

 

演示代碼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上。

#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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM