多執行緒(一)

lethe1203發表於2024-03-19

1、執行緒與程序

程序:一個正在執行的程式,是資源分配的最小單位
1)程序中的事情需要按照一定的順序逐個執行,那麼如何讓一個程序中的一些事情同時執行?
2)程序出現了很多弊端:一是由於程序是資源擁有者,建立、撤銷與切換存在較大的時空開銷,因此需要引入輕量級程序;二是由於多處理器(SMP)出現,可以滿足多個執行單位,而多個程序並行開銷過大。
執行緒:又稱為輕量級程序,程式執行的最小單位,系統排程和分配cpu的基本單位,他是程序中的一個實體。一個程序中可以有多個執行緒,這些執行緒共享程序的所有資源,執行緒本身只包含一點必不可少的資源。
程序負責申請資源,從主函式開始就是一個執行緒了。
程序申請資源,多執行緒中的各個執行緒共享執行緒中的資源
程序申請資源,多程序使用fork()函式來進行建立子程序
1、fork複製會消耗資源
2、程序間通訊還要經過管道,訊息佇列,訊號量來進行通訊比較複雜
程序與執行緒之間的區別:
1)程序有自己獨立的地址空間,多個執行緒共用同一個地址空間
2)執行緒更加節省系統資源
3)在一個地址空間中多個執行緒共享,每個執行緒都有屬於自己的棧區
4)每一個地址空間中多個執行緒獨享,程式碼區、堆區、資料區、開啟的檔案(檔案描述符)都是執行緒共享的
5)每個程序對應一個虛擬地址空間,一個程序只能搶一個CPU時間片
6)在一個地址空間中可以劃分出多個執行緒,在有效的資源基礎上,能夠搶到更多的CPU時間片
CPU排程和切換:執行緒上下文切換比程序要快的多
上下文切換:程序/執行緒分時複用CPU時間片,在切換之前會將上一次任務的狀態進行儲存,下次切換回這個任務的時候,載入這個狀態繼續執行,任務從儲存到再次載入的這個過程就是一次上下文切換。
程序更加廉價,啟動速度更快,退出也快,對系統資源的衝擊較小
在處理多工程式的時候使用多執行緒比使用多程序要更有優勢,但是並不是執行緒並不是越多越好。

2、執行緒的一些術語

1、併發是指同一時刻,只有一條指令執行,但多個程序指令被快速輪換執行,使得在宏觀上具有多個程序同時執行的效果。看起來同時發生,針對單核處理器的
2、並行是指在同一時刻,有多條指令在多個處理器上(cpu)同時執行。真正的同時發生
3、同步:彼此有依賴關係的呼叫不應該 “同時發生”,而同步就是要阻止那些 “同時發生” 的事情
4、非同步:非同步的概念與同步相對,任何兩個彼此獨立的操作是非同步的,它表明事情獨立的發生
多執行緒的優勢:
1、在多處理器中開發程式的並行性
2、在等待IO操作時,程式可以執行其他操作,提高併發性
3、模組化的程式設計,能更清晰的表達程式中獨立事件的關係,結構清晰
4、佔用較少的系統資源,相對於多程序而言
5、多執行緒不一定要多處理器,多處理器只是提高了並行性

3、執行緒建立函式

建立出來的是子執行緒,當單程序程式中建立執行緒的時候,程序便退化成了主執行緒。執行緒與程序的識別符號型別、獲取id的函式、執行緒建立函式分別如下圖所示:
0
pthread_create函式的使用如下:
#include<pthread.h>
 
int  pthread_create(pthread_t *tidp, const  pthread_attr_t *attr,
                            ( void *)(*start_rtn)( void *), void  *arg);
                           
引數說明:
thread:傳出引數、是無符號長整型數,會將執行緒id寫到這個指標指向的記憶體中
attr:執行緒屬性,一般為空
start_rtn:是執行緒執行函式的起始地址         
arg:執行函式的引數     
 
返回值:
執行緒建立成功,則返回0,建立失敗返回錯誤引數   
 
編譯方法:
-lpthread //pthread為動態庫
例如:
gcc test.c -lpthread -o test
測試demo:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
 
void print_id(char *s)
{
        printf("%s :pid is %u tid is %ld\n", s, getpid(), pthread_self());
}
 
void* callback(void *arg)
{
        print_id(arg);
        for (int i = 0; i < 5; i++) {
                printf("子執行緒: i = %d\n", i);
        }
        printf("子執行緒: %ld\n", pthread_self());
 
        return (void *)0;
}
 
int main()
{
        pthread_t tid;
        pthread_create(&tid, NULL, callback, "new thread");
        for (int i = 0; i < 5; i++) {
                printf("主執行緒: i = %d\n", i);
        }
 
        printf("pid is %u tid is %ld\n", getpid(), pthread_self());
        printf("主執行緒: %ld\n", pthread_self());
        // sleep(2);
        
        return 0;
}
執行結果一
主執行緒: i = 0
主執行緒: i = 1
主執行緒: i = 2
主執行緒: i = 3
主執行緒: i = 4
pid is 23801 tid is 139896767649536
主執行緒: 139896767649536
當我們開啟sleep(2)的註釋,執行結果二如下:
主執行緒: i = 0
主執行緒: i = 1
主執行緒: i = 2
主執行緒: i = 3
主執行緒: i = 4
pid is 23892 tid is 140477647664896
主執行緒: 140477647664896
new thread :pid is 23892 tid is 140477639358208
子執行緒: i = 0
子執行緒: i = 1
子執行緒: i = 2
子執行緒: i = 3
子執行緒: i = 4
子執行緒: 140477639358208
解釋:
程式從main函式開始執行,執行到pthread_create函式時,會建立callback的子執行緒執行callback函式里面的相關程式碼,同時main函式里面也繼續向下執行,main函式執行完畢後就會釋放掉相關虛擬地址空間資源,這時候callback子執行緒還沒有執行完,這時就會執行出現結果一。當我們在主執行緒中sleep(2);就可以延長虛擬地址空間的生命週期,就可以正常執行完子執行緒的相關內容了。

4、執行緒退出函式

在編寫多執行緒程式時,如果想讓執行緒退出,但是不會導致虛擬地址空間資源的釋放,我們就可以呼叫執行緒庫中的執行緒退出函式,只要呼叫該函式當前執行緒就立馬退出了,並且不會影響到其他執行緒的正常執行,不管是子執行緒還是主執行緒中都適用。
#include <pthread.h>

void pthread_exit(void *retval);

引數說明:
retval:void*型別的指標,指向的資料將作為執行緒退出時的返回值。如果執行緒不需要返回任何資料,直接設為NULL即可
測試demo:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
 
void print_id(char *s)
{
        printf("%s :pid is %u tid is %ld\n", s, getpid(), pthread_self());
}
 
void* callback(void *arg)
{
        print_id(arg);
        for (int i = 0; i < 5; i++) {
                printf("子執行緒: i = %d\n", i);
        }
        printf("子執行緒: %ld\n", pthread_self());
 
        return (void *)0;
}
 
int main()
{
        pthread_t tid;
        pthread_create(&tid, NULL, callback, "new thread");
 
        printf("pid is %u tid is %ld\n", getpid(), pthread_self());
        printf("主執行緒: %ld\n", pthread_self());
        pthread_exit(NULL);        
 
        return 0;
}
執行結果:
pid is 25055 tid is 140031551674112
主執行緒: 140031551674112
new thread :pid is 25055 tid is 140031543367424
子執行緒: i = 0
子執行緒: i = 1
子執行緒: i = 2
子執行緒: i = 3
子執行緒: i = 4
子執行緒: 140031543367424
將第三點建立執行緒中的例子對比可以看出,pthread_exit函式將主執行緒退出了,但是並沒有釋放虛擬地址空間資源。

5、執行緒回收函式

執行緒和程序一樣,子執行緒退出的時候其核心資源主要由主執行緒回收,執行緒庫中提供的執行緒回收函式是pthread_join(),這個函式是一個阻塞函式,如果還有子執行緒執行,呼叫該函式就會阻塞,子執行緒退出函式解除阻塞進行資源的回收,函式被呼叫一次,只能回收一個子執行緒,如果有多個子執行緒則需要迴圈進行回收操作。
另外透過執行緒回收函式還可以獲取到子執行緒退出時傳遞出來的資料,如下:
#include <pthread.h>
 
int pthread_join(pthread_t thread, void **retval);
 
引數說明:
thread:等待退出執行緒的執行緒號
retval:退出執行緒的返回值,二級指標,是一個傳出函式,這個地址中儲存了pthread_exit()傳遞出的資料,如果不需要,可以為NULL
 
返回值:執行緒回收成功返回0,回收失敗返回錯誤號
測試demo:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
 
struct test
{
        int num;
        int age;
};
 
struct test t;        // 全域性變數多執行緒共享
 
void print_id(char *s)
{
        printf("%s :pid is %u tid is %ld\n", s, getpid(), pthread_self());
}
 
void* callback(void *arg)
{
        print_id(arg);
        for (int i = 0; i < 5; i++) {
                printf("子執行緒: i = %d\n", i);
        }
 
        // struct test t;    棧區被釋放了,所以test不能是區域性變數,定義1
        t.num = 100;
        t.age = 50;
 
        pthread_exit(&t);
 
        printf("子執行緒: %ld\n", pthread_self());
 
        return (void *)0;
}
 
int main()
{
        pthread_t tid;
        pthread_create(&tid, NULL, callback, "new thread");
 
        printf("pid is %u tid is %ld\n", getpid(), pthread_self());
        printf("主執行緒: %ld\n", pthread_self());
 
        void *ptr;        // ptr指向pthread_exit退出時t的地址
        pthread_join(tid, &ptr);
        struct test *pt = (struct test*)ptr;
        printf("num: %d, age = %d\n", pt->num, pt->age);
 
        return 0;
}
執行結果:
pid is 26531 tid is 139762155865856
主執行緒: 139762155865856
new thread :pid is 26531 tid is 139762147559168
子執行緒: i = 0
子執行緒: i = 1
子執行緒: i = 2
子執行緒: i = 3
子執行緒: i = 4
num: 100, age = 50
分析:test t不能定義在1處,因為test t是子執行緒裡面的棧記憶體,在一塊虛擬記憶體空間中,只有一個棧,這個棧被多個子執行緒均分了,一個子執行緒退出,子執行緒所使用的棧就被釋放了,我們取出來的其實是隨機數。如果保證資料正確呢?要保證這塊地址不被釋放就可以了,可以使用堆記憶體,也可以是全域性變數,因為多執行緒共享全域性資料區和堆區,只要保證多個執行緒能夠訪問這塊記憶體就可以了。
測試demo2:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
 
struct test
{
        int num;
        int age;
};
 
// struct test t;        // 全域性變數多執行緒共享
 
void print_id(char *s)
{
        printf("pid is %u tid is %ld\n", getpid(), pthread_self());
}
 
void* callback(void *arg)
{
        print_id(arg);
        for (int i = 0; i < 5; i++) {
                printf("子執行緒: i = %d\n", i);
        }
 
        struct test*t = (struct test*)arg;
        // struct test t;    棧區被釋放了,所以test不能是區域性變數
        t->num = 100;
        t->age = 50;
 
        pthread_exit(t);
 
        printf("子執行緒: %ld\n", pthread_self());
 
        return (void *)0;
}
 
int main()
{
        struct test t;
        pthread_t tid;
        pthread_create(&tid, NULL, callback, &t);
 
        printf("pid is %u tid is %ld\n", getpid(), pthread_self());
        printf("主執行緒: %ld\n", pthread_self());
 
        void *ptr;        // ptr指向pthread_exit退出時t的地址
        pthread_join(tid, &ptr);
        struct test *pt = (struct test*)ptr;
        printf("num: %d, age = %d\n", pt->num, pt->age);
 
        return 0;
}
執行結果:
pid is 27333 tid is 140069782001408
主執行緒: 140069782001408
pid is 27333 tid is 140069773694720
子執行緒: i = 0
子執行緒: i = 1
子執行緒: i = 2
子執行緒: i = 3
子執行緒: i = 4
num: 100, age = 50
分析:多個執行緒把棧空間給平分了,多執行緒中不能訪問相應的棧空間的,但是主動將主執行緒的棧空間傳遞給子執行緒,那麼就可以訪問了,主執行緒和子執行緒都是同一個虛擬地址空間的,所以可以訪問到。當子執行緒釋放掉,主執行緒棧空間還是存在的,呼叫的時候是可以正常執行的。

6、執行緒分離函式

一般情況下,程式中的主執行緒有屬於自己的處理流程,如果讓主執行緒負責子執行緒的資源回收,呼叫pthread_join()只要子執行緒不退出就會被一直阻塞,那麼主執行緒的任務也不能被執行了
線上程庫函式中為我們提供了執行緒分離函式pthread_detach(),呼叫這個函式之後指定的子執行緒可以跟主執行緒分離,當子執行緒退出的時候,其佔用的核心資源就被系統的其他程序接管並回收了。執行緒分離之後主執行緒中使用pthread_join()就回收不到子執行緒資源了。
#include <pthread.h>
int pthread_detach(pthread_t thread);

引數解釋:
thread:執行緒id
測試demo:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>

struct test
{
        int num;
        int age;
};
 
void print_id(char *s)
{
        printf("pid is %u tid is %ld\n", getpid(), pthread_self());
}
 
void* callback(void *arg)
{
        print_id(arg);
        for (int i = 0; i < 5; i++) {
                printf("子執行緒: i = %d\n", i);
        }
 
        struct test*t = (struct test*)arg;
        t->num = 100;
        t->age = 50;
 
        pthread_exit(t);
 
        printf("子執行緒: %ld\n", pthread_self());
 
        return (void *)0;
}
 
int main()
{
        struct test t;
        pthread_t tid;
        pthread_create(&tid, NULL, callback, &t);
 
        printf("pid is %u tid is %ld\n", getpid(), pthread_self());
        printf("主執行緒: %ld\n", pthread_self());
 
        pthread_detach(tid);
        pthread_exit(NULL);
 
        return 0;
}
執行結果:
pid is 28021 tid is 140366746593024
主執行緒: 140366746593024
pid is 28021 tid is 140366738286336
子執行緒: i = 0
子執行緒: i = 1
子執行緒: i = 2
子執行緒: i = 3
子執行緒: i = 4
分析:使用pthread_detach函式就可以直接將子執行緒與主執行緒的分離,子執行緒函式執行完後退出,由核心的函式自動回收,主執行緒也不會被阻塞。

7、執行緒取消函式

執行緒取消函式的意思就是在某些特定的情況下在一個執行緒中殺死另外一個執行緒,使用這個函式殺死另外一個執行緒需要分兩步:
1)線上程A中呼叫執行緒取消函式pthread_cancel,指定取消執行緒B,這是B是被取消不了的
2)執行緒B進行一次系統呼叫(用使用者態切換到核心態),否則執行緒B可以一直執行
#include <pthread.h>
int pthread_cancel(pthread_t pid);

引數:執行緒號
返回值:成功返回0,失敗返回錯誤碼
第二點比如執行printf進行列印,printf最終會寫終端,因此在底層會呼叫read方法。

8、執行緒ID比較函式

在linux中執行緒ID本質就是一個無符號長整形,因此可以直接使用比較運算子比較兩個執行緒的ID:
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
返回值:ID相等返回值不等於0,不相等返回值等於0

相關文章