首頁 > 軟體

Linux之執行緒的建立方式

2023-03-23 22:07:17

執行緒的概念與實現方式

執行緒是程序內部的一條執行序列或執行路徑,一個程序可以包含多條執行緒。

  • 從資源分配的角度來看,程序是作業系統進行資源分配的基本單位。
  • 從資源排程的角度來看,執行緒是資源排程的最小單位,是程式執行的最小單位

執行序列就是一組有序指令的集合——函數。

執行緒是程序內部的一條執行序列,一個程序至少有一條執行緒,稱之為主執行緒(main方法代表的執行序列),可以通過執行緒庫建立其他執行緒(給執行緒制定一個它要執行的函數),將建立的執行緒稱之為函數執行緒。

執行緒的實現方式

  • 核心級執行緒(由核心直接建立和管理執行緒,雖然建立開銷較大,但是可以利用多處理器的資源)
  • 使用者級執行緒(由執行緒庫建立和管理多個執行緒,執行緒的實現都是在使用者態,核心無法感知,建立開銷較小,無法使用多處理器的資源)
  • 混合級執行緒(結合以上兩種方式實現,可以利用多處理器的資源,從而在使用者空間中建立更多的執行緒,從而對映到核心空間的執行緒中,多對多,N:M(N>>M))

Linux系統實現多執行緒的方式

Linux 實現執行緒的機制非常獨特。從核心的角度來說,它並沒有執行緒這個概念。

Linux 把所有的執行緒都當做程序來實現。核心並沒有準備特別的排程演演算法或是定義特別的資料結構來表徵執行緒。

相反,執行緒僅僅被視為一個與其他程序共用某些資源的程序。

每個執行緒都擁有唯 一隸屬於自己的task_struct,所以在核心中,它看起來就像是一個普通的程序(只是執行緒和 其他一些程序共用某些資源,如地址空間)

執行緒和程序的區別

  • 程序是資源分配最小單位,執行緒是程式執行的最小單位;
  • 執行緒間的切換效率相比程序間的切換要高
  • 程序有自己獨立的地址空間,每啟動一個程序,系統都會為其分配地址空間,建立資料表來維護程式碼段、堆疊段和資料段,執行緒沒有獨立的地址空間,它使用相同的地址空間共用資料;
  • 建立一個執行緒比程序開銷小;
  • 執行緒佔用的資源要⽐程序少很多。
  • 執行緒之間通訊更方便,同一個程序下,執行緒共用全域性變數,靜態變數等資料,程序之間的通訊需要以通訊的方式(IPC)進行;(但多執行緒程式處理好同步與互斥是個難點)
  • 多程序程式更安全,生命力更強,一個程序死掉不會對另一個程序造成影響(源於有獨立的地址空間),多執行緒程式更不易維護,一個執行緒死掉,整個程序就死掉了(因為共用地址空間);
  • 程序對資源保護要求高,開銷大,效率相對較低,執行緒資源保護要求不高,但開銷小,效率高,可頻繁切換;

多執行緒開發的三個基本概念

  • 執行緒 【建立、退出、等待】
  • 互斥鎖【建立、銷燬、加鎖】、解鎖】
  • 條件【建立、銷燬、觸發、廣播、等待】

執行緒庫的使用

1.建立執行緒

#include<phread.h>

int pthread_create(pthread_t *id , pthread_attr_t *attr, void(*fun)(void*), void *arg);
  • id :傳遞一個pthread_t型別的變數的地址,建立成功後,用來獲取新建立的執行緒的TID
  • attr:指定執行緒的屬性 預設使用NULL
  • fun:執行緒函數的地址
  • arg:傳遞給執行緒函數的引數
  • 返回值,成功返回0,失敗返回錯誤碼

多執行緒程式碼範例

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>

#include<pthread.h>

//宣告一個執行緒函數
void *fun(void *);

int main()
{
	printf("main startn");

	pthread_t id;
	//建立函數執行緒,並且指定函數執行緒要執行的函數
	int res = pthread_create(&id,NULL,fun,NULL);
	assert(res == 0);

	//之後並行執行
	int i = 0;	
	for(; i < 5; i++)
	{
		printf("main runningn");
		sleep(1);
	}

	printf("main overn");
	exit(0);
}

//定義執行緒函數
void* fun(void *arg)
{
	printf("fun startn");

	int i = 0;
	for(; i < 3;i++)
	{
		printf("fun runningn");
		sleep(1);
	}

	printf("fun overn");
}

gcc編譯程式碼時報`undifined reference to xxxxx錯誤,都是因為程式中呼叫了一些方法,但是沒有連線該方法所在的檔案,例如下面的情況:

連線庫檔案編譯成功並執行,這一點在幫助手冊中也有提示:Compile and link with -pthread

比較兩次執行的結果發現前三條執行語句時一樣的

結論

  • 建立執行緒並執行執行緒函數,和呼叫函數是完全不同的概念。
  • 主執行緒和函數執行緒是並行執行的。
  • 執行緒提前於主執行緒結束時,不會影響主執行緒的執行
  • 主執行緒提前於執行緒結束時,整個程序都會結束,其他執行緒也會結束
  • 建立函數執行緒後,哪個執行緒先被執行是有作業系統的排程演演算法和機器環境決定。

函數執行緒在主執行緒結束後也隨之退出,原因:主執行緒結束時使用的是exit方法,這個方法結束的是程序。

然而修改程式碼為:pthread_exit(NULL);此時主執行緒結束,函數執行緒會繼續執行直至完成。即便如此,我們還是不推薦大家手動結束主執行緒,我們更喜歡讓主執行緒等待一會。

給執行緒函數傳參

①值傳遞

將變數的值直接轉成void*型別進行傳遞

因為執行緒函數接受的是一個void*型別的指標,只要是指標,32位元系統上都是4個位元組,值傳遞就只能傳遞小於或等於4位元組的值。

程式碼範例

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>

#include<pthread.h>

void *fun(void *);

int main()
{
	printf("main startn");

	int a = 10;
	
	pthread_t id;
	int res = pthread_create(&id,NULL,fun,(void*)a);
	assert(res == 0);

	int i = 0;	
	for(; i < 5; i++)
	{
		printf("main runningn");
		sleep(1);
	}

	printf("main overn");
	exit(0);
}


void* fun(void *arg)
{
	int b = (int)arg;
	printf("b == %dn",b);
}

②地址傳遞

將變數(所有型別)的地址強轉成void*型別進行傳遞,就和在普通函數呼叫傳遞變數的地址相似。

主執行緒和函數執行緒通過這個地址就可以共用地址所指向的空間。

一個程序內的所有執行緒是共用這個程序的地址空間。

多執行緒下程序的4G虛擬地址空間

一個程序內的所有執行緒對於全域性資料,靜態資料,堆區空間都是共用的。

執行緒之間傳遞資料很簡單,但是隨之帶來的問題就是執行緒並行執行時無法保證執行緒安全。

程式碼範例

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>

#include<pthread.h>

int gdata = 10; //.data

void *fun(void *);

int main()
{
	int *ptr = (int *)malloc(4);//.heap
    *ptr = 10;
	
	pthread_t id;
	int res = pthread_create(&id,NULL,fun,(void*)ptr);
	assert(res == 0);

    sleep(2);//等待兩秒,保證函數執行緒已經講資料修改

	printf("main : gdata == %dn",gdata);
    printf("main : *ptr = %dn",*ptr);

	exit(0);
}


void *fun(void *arg)
{
	int *p = (int*)arg;

    gdata = 20000;
    *p = 20;

	printf("fun overn");
}

執行緒庫中的其他方法

執行緒退出的三種方式:

  • 執行緒從執行函數返回,返回值是執行緒的退出碼;
  • 執行緒被同一程序的其他執行緒取消;
  • 呼叫pthread_exit()函數退出;

等待執行緒終止

int pthread_join(pthread_t thread, void **retval);
args:
    pthread_t thread: 被連線執行緒的執行緒號,該執行緒必須位於當前程序中,而且不得是分離執行緒
    void **retval :該引數不為NULL時,指向某個位置 在該函數返回時,將該位置設定為已終止執行緒的退出狀態
    return:
    執行緒連線的狀態,0是成功,非0是失敗

當A執行緒呼叫執行緒B並 pthread_join() 時,A執行緒會處於阻塞狀態,直到B執行緒結束後,A執行緒才會繼續執行下去。當 pthread_join() 函數返回後,被呼叫執行緒才算真正意義上的結束,它的記憶體空間也會被釋放(如果被呼叫執行緒是非分離的)。

這裡有三點需要注意:

  • 1.被釋放的記憶體空間僅僅是系統空間,你必須手動清除程式分配的空間,比如 malloc() 分配的空間。
  • 2.一個執行緒只能被一個執行緒所連線。
  • 3.被連線的執行緒必須是非分離的,否則連線會出錯。所以可以看出pthread_join()有兩種作用:1-用於等待其他執行緒結束:當呼叫 pthread_join() 時,當前執行緒會處於阻塞狀態,直到被呼叫的執行緒結束後,當前執行緒才會重新開始執行。2-對執行緒的資源進行回收:如果一個執行緒是非分離的(預設情況下建立的執行緒都是非分離)並且沒有對該執行緒使用 pthread_join() 的話,該執行緒結束後並不會釋放其記憶體空間,這會導致該執行緒變成了“殭屍執行緒”。

等待指定的子執行緒結束

  • 等待thread()指定的執行緒退出,執行緒未退出時,該方法阻塞
  • result接收thread執行緒退出時,指定退出資訊
int pthread_join(pthread_t id,void **result)//呼叫這個方法的執行緒會阻塞,直到等待執行緒結束

程式碼演示:

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>

#include<pthread.h>

int main()
{
	printf("main startn");

	pthread_t id;
	int res = pthread_create(&id,NULL,fun,NULL);
	assert(res == 0);

	//之後並行執行
	int i = 0;	
	for(; i < 5; i++)
	{
		printf("main runningn");
		sleep(1);
	}
	
	char *s = NULL;
	pthread_join(id,(void **)&s);
	printf("join : s = %sn",s);
	
	exit(0);
}

//定義執行緒函數
void* fun(void *arg)
{
	printf("fun startn");

	int i = 0;
	for(; i < 10;i++)
	{
		printf("fun runningn");
		sleep(1);
	}

	printf("fun overn");

	pthread_exit("fun over");//將該字元常數返回給主執行緒
}

此時,主執行緒完成五次輸出,就會等待子執行緒結束,阻塞等待,子執行緒結束後,最後,主執行緒列印join:s = fun over

關於exit和join的一些詳細說明:

  • 執行緒自己執行結束,或者呼叫pthread_exit結束,執行緒都會釋放自己獨有的空間資源;
  • 若執行緒是非分離的,執行緒會保留執行緒ID號,直到其他執行緒通過joining這個執行緒確認其已經死亡,join的結果是joining執行緒得到已終止執行緒的退出狀態,已終止執行緒將消失;
  • 若執行緒是分離的,不需要使用pthread_exit(),執行緒自己執行結束,執行緒結束就會自己釋放所有空間資源(包括執行緒ID號);
  • 子執行緒最終一定要使用pthread_join()或者設定為分離執行緒來結束執行緒,否則執行緒的資源不會被完全釋放(使用取消執行緒功能也不能完全釋放);
  • 主執行緒執行pthrea_exit(),會結束主執行緒,但是不會結束子執行緒;
  • 主執行緒結束,則整個程式結束,所以主執行緒最好使用pthread_join函數等待子執行緒結束,使用該函數一個執行緒可以等待多個執行緒結束;
  • 使用pthread_join函數的執行緒將會阻塞,直到被join的函數執行緒結束,該函數返回,但是它對被等待終止的執行緒執行沒有影響;
  • 如果子執行緒使用exit()則可以結束整個程序;

執行緒屬性

執行緒具有的屬性可以線上程建立的時候指定;

——pthread_create()函數的第二個引數(pthread_attr_t *attr)表示執行緒的屬性,在以前的例子中將其值設為NULL,也就是採用預設屬性,執行緒的多項屬性都是可以修改的,這些屬性包括繫結屬性,分離屬性,堆疊屬性,堆疊大小,優先順序。

系統預設的是非繫結,非分離,預設1M的堆疊以及父子程序優先順序相同

執行緒結構如下:

typedef struct
{
    int             detachstate;     //執行緒的分離狀態
    int             schedpolicy;    //執行緒排程策略
    struct sched_param  schedparam; //執行緒的排程引數
    int             inheritsched;   //執行緒的繼承性
    int             scope;      //執行緒的作用域
    size_t          guardsize;  //執行緒棧末尾的警戒緩衝區大小
    int             stackaddr_set; //執行緒的棧設定
    void*           stackaddr;  //執行緒棧的位置
    size_t          stacksize;  //執行緒棧的大小
} pthread_attr_t;

每一個屬性都有對應的一些函數,用於對其進行檢視和修改,下面分別介紹:

執行緒屬性初始化

初始化和去初始化分別對應於如下的兩個函數:

#include <pthread.h>
​
①int pthread_attr_init(pthread_attr_t *attr);
②it pthread_attr_destroy(pthread_attr_t *attr);

①功能:

  • 初始化執行緒屬性函數,注意:應先初始化執行緒屬性,再pthread_create建立執行緒

引數:

  • attr:執行緒屬性結構體

返回值:

  • 成功:0
  • 失敗:-1

②​功能:

  • 銷燬執行緒屬性所佔用的資源函數

引數:

  • attr:執行緒屬性結構體

返回值:

  • 成功:0
  • 失敗:-1

執行緒分離

執行緒的分離狀態決定一個執行緒以什麼樣的方式來終止自己,這個在之前我們也說過了。

  • 非分離狀態:執行緒的預設屬性是非分離狀態,這種情況下,原有的執行緒等待建立的執行緒結束。只有當pthread_join()函數返回時,建立的執行緒才算終止,才能釋放自己佔用的系統資源。
  • 分離狀態:分離執行緒沒有被其他的執行緒所等待,自己執行結束了,執行緒也就終止了,馬上釋放系統資源。應該根據自己的需要,選擇適當的分離狀態。

相關API如下:

#include <pthread.h>
​
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

功能:設定執行緒分離狀態

引數:

  • attr:已初始化的執行緒屬性
  • detachstate: 分離狀態

PTHREAD_CREATE_DETACHED(分離執行緒)

PTHREAD_CREATE_JOINABLE(非分離執行緒)

返回值:

  • 成功:0
  • 失敗:非0

int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);

功能:獲取執行緒分離狀態

引數:

  • attr:已初始化的執行緒屬性detachstate: 分離狀態

PTHREAD_CREATE_DETACHED(分離執行緒)

PTHREAD _CREATE_JOINABLE(非分離執行緒)

返回值:

  • 成功:0
  • 失敗:非0

注意:

當一個執行緒被設定為分離執行緒時,假設此時該執行緒的執行速度非常快,它很可能在pthread_create返回之前就終止; 終止之後將執行緒號和系統資源移交給其他執行緒使用,這樣呼叫create就得到了錯誤的執行緒號,因此就必須採取一些同步措施,可以在被建立的執行緒裡呼叫pthread_cond_timedwait函數,讓這個執行緒等待一會兒,留出足夠的時間讓函數pthread_create返回,設定一段等待時間,是在多執行緒程式設計裡常用的方法。但是注意不要使用諸如wait()之類的函數,它們是使整個程序睡眠,並不能解決執行緒同步的問題。

總結

以上為個人經驗,希望能給大家一個參考,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com