線程的概念、優缺點和創建步驟
1. 什麼是線程
線程的概念:輕量級的進程,一個進程內部可以有多個線程,默認情況下一個進程只有一個線程
線程是最小的執行單位,進程是最小的系統資源分配單位
線程就是輕量級的進程,如果說下圖中的a.out
,是一個進程,那裏面的那幾根"彎彎曲曲"的線,就是所謂的線程,一般來說,一個進程中只有一個線程,當然,一個進程中也可以有多個線程,這種狀況就叫做多線程,當然從圖上也可以看出來,一個進程中的多個線程,也是公用的相同的資源的:
因爲線程是公用進程的空間的,那麼可以理解爲,在內存上,除了棧(stack)這塊,其他地方都是共享的!(上圖的左側所示),這是因爲線程是有自己的執行目的的,每個線程的任務是非常明確的,後面會提到,一個線程實際上就是執行的一個函數,因爲函數是存儲在棧中的,所以這塊是沒法共享的,要進行區分,除此之外,其他區域都是可以共享的。
線程的好處也就可以看出來了,線程都在一個進程內部,隨便一個變量所有線程都可以用;線程可以更有效的理由CPU(但是,要說的是,如果電腦只有一塊CPU,一個核心數,線程再多也沒辦法同時"幹活兒",因爲在一個進程中,只能有一個“線程”在運行的(可以把線程理解成一個函數)),總之,多線程和多進程,都是爲了更充分的利用CPU。
那麼再做個關於"進程"和"線程"的形象的比喻,一個工廠,但可供的電量有限,只能供應三個車間,錯開開工,不能同時開工,那麼這個工廠,就好比是cpu,這三個車間,可以理解成"進程",那麼車間裏面的每個人(苦力,真正幹活的),就相當於"線程"。那麼每個"線程"都有非常明確的分工。在車間裏的設備和資源,對每個人(線程)來說也都是公有的。
線程是最小的執行單位,進程是最小的系統資源分配單位 ,從內核角度看,進程和線程是沒有區分的,內核實現都是通過 clone 函數實現的,線程也有自己的PCB
2. 線程共享資源與非共享資源
儘量不要讓線程與信號放在一起使用,避免亂上加亂
線程共享資源:
- 文件描述符表
- 每種信號的處理方式
- 當前工作目錄
- 用戶ID和組ID
- 內存地址空間 (.text/.data/.bss/heap/共享庫)
線程非共享資源:
- 線程id
- 處理器現場和棧指針(內核棧)
- 獨立的棧空間(用戶空間棧)
- errno變量(每個線程有自己獨有的errno變量)
- 信號屏蔽字
- 調度優先級(可以設置線程的優先級)
每個線程有自己的errno
。
通過如下函數可以獲得錯誤碼對應的錯誤信息
char *strerror(int errnum);
3. 線程的優缺點
優點:
- 提高程序併發性(爲了更好的利用CPU)
- 開銷小(不必再申請空間,直接用的是進程的空間)
- 數據通信、共享數據方便(在一個進程中的線程,可以共享進程中所創建的變量來共用)
缺點:
- 庫函數,不穩定 (因爲早期Unix並沒有線程的概念,線程概念是後加的,所以,是放在了庫函數中)
- 調試、編寫困難(可以讓程序員後天學習克服)
- 對信號支持不好(大不了就不用信號了)
彙總:優點相對突出,缺點均不是硬傷。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. 線程使用注意事項
- 主線程退出其他線程不退出,主線程應調用pthread_exit。
- 避免殭屍進程
pthread_join
pthread_detach
pthread_create指定分離屬性
被join線程可能在join函數返回前就釋放完自己的所有內存資源,所以不應當返回被回收線程棧中的值。 - malloc和mmap申請的內存可以被其他線程釋放。
- 應避免在多線程模型中調用fork,除非馬上exec,子進程中只有調用fork的線程存在,其他線程在子進程中均pthread_exit
- 信號的複雜語義很難和多線程共存,應避免在多線程引入信號機制。
- 編譯要指定
-lpthread