線程的概念、優缺點和創建步驟

在這裏插入圖片描述

1. 什麼是線程

線程的概念:輕量級的進程,一個進程內部可以有多個線程,默認情況下一個進程只有一個線程
線程是最小的執行單位,進程是最小的系統資源分配單位
  線程就是輕量級的進程,如果說下圖中的a.out,是一個進程,那裏面的那幾根"彎彎曲曲"的線,就是所謂的線程,一般來說,一個進程中只有一個線程,當然,一個進程中也可以有多個線程,這種狀況就叫做多線程,當然從圖上也可以看出來,一個進程中的多個線程,也是公用的相同的資源的:
在這裏插入圖片描述
  因爲線程是公用進程的空間的,那麼可以理解爲,在內存上,除了棧(stack)這塊,其他地方都是共享的!(上圖的左側所示),這是因爲線程是有自己的執行目的的,每個線程的任務是非常明確的,後面會提到,一個線程實際上就是執行的一個函數,因爲函數是存儲在棧中的,所以這塊是沒法共享的,要進行區分,除此之外,其他區域都是可以共享的。

  線程的好處也就可以看出來了,線程都在一個進程內部,隨便一個變量所有線程都可以用;線程可以更有效的理由CPU(但是,要說的是,如果電腦只有一塊CPU,一個核心數,線程再多也沒辦法同時"幹活兒",因爲在一個進程中,只能有一個“線程”在運行的(可以把線程理解成一個函數)),總之,多線程和多進程,都是爲了更充分的利用CPU。

  那麼再做個關於"進程"和"線程"的形象的比喻,一個工廠,但可供的電量有限,只能供應三個車間,錯開開工,不能同時開工,那麼這個工廠,就好比是cpu,這三個車間,可以理解成"進程",那麼車間裏面的每個人(苦力,真正幹活的),就相當於"線程"。那麼每個"線程"都有非常明確的分工。在車間裏的設備和資源,對每個人(線程)來說也都是公有的。
在這裏插入圖片描述
  線程是最小的執行單位,進程是最小的系統資源分配單位 ,從內核角度看,進程和線程是沒有區分的,內核實現都是通過 clone 函數實現的,線程也有自己的PCB

2. 線程共享資源與非共享資源

儘量不要讓線程與信號放在一起使用,避免亂上加亂
線程共享資源:

  1. 文件描述符表
  2. 每種信號的處理方式
  3. 當前工作目錄
  4. 用戶ID和組ID
  5. 內存地址空間 (.text/.data/.bss/heap/共享庫)

線程非共享資源:

  1. 線程id
  2. 處理器現場和棧指針(內核棧)
  3. 獨立的棧空間(用戶空間棧)
  4. errno變量(每個線程有自己獨有的errno變量)
  5. 信號屏蔽字
  6. 調度優先級(可以設置線程的優先級)

每個線程有自己的errno

通過如下函數可以獲得錯誤碼對應的錯誤信息

char *strerror(int errnum);

3. 線程的優缺點

優點:

  1. 提高程序併發性(爲了更好的利用CPU)
  2. 開銷小(不必再申請空間,直接用的是進程的空間)
  3. 數據通信、共享數據方便(在一個進程中的線程,可以共享進程中所創建的變量來共用)

缺點:

  1. 庫函數,不穩定 (因爲早期Unix並沒有線程的概念,線程概念是後加的,所以,是放在了庫函數中)
  2. 調試、編寫困難(可以讓程序員後天學習克服)
  3. 對信號支持不好(大不了就不用信號了)

彙總:優點相對突出,缺點均不是硬傷。Linux下由於實現方法導致進程、線程差別不是很大。

4. 創建一個線程

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

在這裏插入圖片描述

thread 		線程的ID,傳出參數
attr 		代表線程的屬性
第三個參數  	函數指針, void *func(void*)
arg 		線程執行函數的參數
返回值  		成功  0   失敗  errno

編譯時需要加 -lpthread
注意:線程ID在進程內是唯一的,但是在整個操作系統內部不一定是唯一的。

#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
 
void *thr(void* arg){
	//無符號長整型數  %lu
	printf("I am a thread! pid=%d, tid=%lu\n", getpid(), pthread_self());
}
 
int main(){
	pthread_t tid;
	pthread_create(&tid, NULL, thr, NULL);
	printf("I am main thread, pid=%d, tid=%lu\n", getpid(), pthread_self());
	sleep(1);
	return 0;
}

在這裏插入圖片描述上面的代碼存在一個問題:
就是如果主線程不睡眠,則另一個線程沒有機會去執行(因爲主線程打印之後就直接執行return 0了)。
在這裏插入圖片描述
將上面代碼中的sleep(1)換成如下語句即可:

pthread_exit(NULL);

4.1 線程退出函數pthread_exit

在這裏插入圖片描述注意事項:

  • 線程中使用pthread_exit 來退出線程
  • 線程中可以用 return(主控線程不行)
  • exit代表退出整個進程

4.2 線程回收pthread_join

線程回收函數,阻塞等待

int pthread_join(pthread_t thread, void **retval);
thread 		創建的時候傳出的第一個參數
retval 		代表的傳出線程的退出信息(就是返回值)
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
 
void *thr(void *arg){
	printf("I am a thread, tid=%lu\n", pthread_self());
	sleep(5);
	printf("I am a thread, tid=%lu\n", pthread_self());
	return (void*)100;
}
 
int main(){
	pthread_t tid;
	pthread_create(&tid, NULL, thr, NULL);
	void *ret;
	//線程回收函數
	pthread_join(tid, &ret);
	printf("ret exit with %d\n",(int)ret);
	pthread_exit(NULL);
	return 0;
}

在這裏插入圖片描述

4.3 殺死線程pthread_cancel

在這裏插入圖片描述

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
 
void *thr(void*arg){
	while(1){
		printf("I am thread, very happy! tid=%lu\n", pthread_self());
		sleep(1);
	}
	return NULL;
}
int main(){
	pthread_t tid;
	pthread_create(&tid, NULL, thr, NULL);
	sleep(5);
	//殺死線程
	pthread_cancel(tid);
	void *ret;
	//阻塞等待回收線程
	pthread_join(tid, &ret);
	printf("thread exit with %d\n", ret);
	return 0;
}

在這裏插入圖片描述如果把上面的代碼中的thr函數中的printf動作和sleep動作註釋之後,線程就殺不死了。因爲pthread_cancle函數需要有一個取消點
如果你的線程函數裏面實在是沒有取消點,你可以加上這樣的一個函數:

pthread_testcancel();

通過這個函數可以強行添加一個取消點

4.4 線程分離pthread_detach

在這裏插入圖片描述

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>
 
void *thr(void *arg){
	printf("I am a thread, self=%lu\n", pthread_self());
	sleep(4);
	printf("I am a thread, self=%lu\n", pthread_self());
	return NULL;
}
 
int main(){
	pthread_t tid;
	pthread_create(&tid, NULL, thr, NULL);
	//線程分離
	pthread_detach(tid);
	sleep(5);
	int ret=0;
	//阻塞失敗
	if((ret=pthread_join(tid, NULL))>0){
		printf("join err:%d, %s\n", ret, strerror(ret));
	}
 
	return 0;
}

在這裏插入圖片描述執行了線程分離之後,pthread_join函數就回收失敗了。

4.5 判斷兩線程ID是否相等pthread_equal

在這裏插入圖片描述

4.6 線程屬性設置分離

創建那種易產生就直接是分離狀的線程,不需要我們去執行detach函數來進程線程分離(因爲執行detach函數還會有一些特殊情況,比如線程創建好之後很快就結束了,此時還沒執行到detach函數)
在這裏插入圖片描述
在這裏插入圖片描述

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
 
void *thr(void *arg) {
    printf("I am a thread \n");
    return NULL;
}
 
int main() {
	//設置線程屬性
    pthread_attr_t attr;
	//初始化屬性
    pthread_attr_init(&attr);
    //設置線程分離屬性,這樣線程創建好之後就直接分開了
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);//設置屬性分離
    pthread_t tid;
	//創建線程 第二個參數是線程屬性
    pthread_create(&tid,&attr,thr,NULL);
    int ret;    
	//阻塞回收線程失敗
    if((ret = pthread_join(tid,NULL)) > 0){
        printf("join err:%d,%s\n",ret,strerror(ret));
    }
	//摧毀屬性
    pthread_attr_destroy(&attr);
    return 0;
}

4.7 創建多個子線程

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
 
void *thr(void *arg){
	int num=(int)arg;
	printf("I am %d thread, self=%lu\n", num, pthread_self());
	return (void*)(100+num);
}
 
int main(){
	pthread_t tid[5];
	for(int i=0; i<5; i++){
		pthread_create(&tid[i], NULL, thr, (void*)i);
	}
	for(int i=0; i<5; i++){
		void *ret;
		pthread_join(tid[i], &ret);
		printf("i =%d, ret=%d\n", i, (int)ret);
	}
	return 0;
}

在這裏插入圖片描述

5. 線程使用注意事項

  1. 主線程退出其他線程不退出,主線程應調用pthread_exit。
  2. 避免殭屍進程
    pthread_join
    pthread_detach
    pthread_create指定分離屬性
    被join線程可能在join函數返回前就釋放完自己的所有內存資源,所以不應當返回被回收線程棧中的值。
  3. malloc和mmap申請的內存可以被其他線程釋放。
  4. 應避免在多線程模型中調用fork,除非馬上exec,子進程中只有調用fork的線程存在,其他線程在子進程中均pthread_exit
  5. 信號的複雜語義很難和多線程共存,應避免在多線程引入信號機制。
  6. 編譯要指定-lpthread
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章