多執行緒基礎操作
本片部落格會貼上部分程式碼,想要了解更多程式碼資訊,可訪問小編的GitHub關於本篇的程式碼
- 對比程序與執行緒的區別
執行緒概念及特點
- Linux下執行緒是以程序模擬的,Linux下的程序控制塊pcb實際就是一個執行緒,其他系統不一定
- Linux對程序和執行緒不作區分,Linux下的執行緒以程序PCB模擬,也就是說task_struct其實就是執行緒,CPU排程的基本單位是執行緒,程序就是執行緒組。
- Linux下的pcb其實就是執行緒的描述,Linux下的執行緒以程序pcb模擬,因此也叫輕量級程序。
- 一個程序中至少有一個執行緒,執行緒是程序內的一條執行流。
執行緒共享程序資料,但也擁有自己的部分資料:
執行緒ID
組暫存器(上下文資料)
棧
errno
訊號遮蔽字
排程優先順序
如果定義1個函式,在各執行緒中都可以呼叫,如果定義1個全域性變數,在各執行緒中都可以訪問到,除此之外,各執行緒還共享以下程序資源和環境:
檔案描述符表
每種訊號的處理方式(SIG_ IGN、SIG_ DFL或者?定義的訊號處理函式)
當前工作目錄
使用者id和組id
Text Segment、Data Segment都是共享的。
程序和執行緒的比較,執行緒優缺點
執行緒的優點:
一個程序中有可能會有多個執行緒,而這些執行緒共享同一個虛擬地址空間,因此有時候也會說執行緒是在程序中的
a、 因此他們共享了整個程式碼段、資料段,執行緒間通訊變得極為方便
b、建立或銷燬一個執行緒相比較於程序來說成本更低(不需要額外建立虛擬地址空間)
c、執行緒的排程切換相較於程序也較低
d、執行緒佔用的資源比程序少很多
e、能夠充分利用多處理器的可並行數量
f、在等待IO操作、CPU計算等任務時候,執行緒支援多處理器並行處理,任務分攤,提高效率。
執行緒的缺點:一個程序中有可能會有多個執行緒,而這些執行緒共享同一個虛擬地址空間
a、 因為執行緒間的資料訪問變得簡單,因此資料安全訪問問題更加突出,要考慮的問題增多,編碼難度增多。資源爭搶問題更加突出。
b、 一些系統呼叫和異常都是針對整個程序的,因此一個執行緒中出現了異常,那麼,整個程序都會受到影響,以及一些系統呼叫的使用也是需要注意的。
程序的優點:安全、穩定(因為程序的獨立性)
- 執行緒相關程式碼,總結執行緒屬性
執行緒的控制
程序是作業系統資源分配的一個基本單位(通過頁表)
執行緒是CPU排程的一個基本單位(CPU排程的是pcb)
執行緒建立:執行緒共享程序地址空間,但是每一個執行緒都有自己相對獨立的一個地址空間
作業系統並沒有提供系統呼叫來建立執行緒,所以就在posix標準庫中,實現了一套執行緒控制(建立、終止、等待…)的介面,因為這套介面建立的執行緒是庫函式,是使用者態的執行緒建立,因此建立的執行緒也叫做使用者執行緒。 一個使用者執行緒對應了一個輕量級程序來進行排程執行的。(對作業系統來說,建立了一個輕量級程序)
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.
pthread_t * thread:建立的執行緒id
const pthread_attr_t:執行緒屬性,一般設定為NULL
void *(*start_routine) (void *):執行緒的執行函式
void *arg:給執行緒的執行函式傳的引數
pthread_create介面建立了一個使用者執行緒,並且通過第一個引數返回了一個使用者的id,這個id數字非常大,其實它就是一個地址,是指向自己的執行緒地址空間在整個程序虛擬地址空間中的位置。
每一個執行緒都需要有自己的棧區,否則如果所有執行緒共用一個棧的話,會引起呼叫棧混亂,並且因為CPU是以pcb來排程的,因此CPU排程的基本單位,所以每一個執行緒也都應該有自己的上下文資料來儲存CPU排程切換時的資料。(而這些都是線上程地址空間中,每一個執行緒都有自己的執行緒地址空間,它們相對來說獨立,但是執行緒地址空間是在虛擬地址空間內的)
#include<pthread.h>
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
void* start(void *arg){
int num = (int)arg;
while(1){
printf("The pthread%d\n",num);
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,start,(void*)999);
//pthread_t pthread_self(void);
// 獲取呼叫執行緒的執行緒id(使用者態執行緒id)tid是pthread_t型別即無符號長整型lu
printf("Main pthread ID:%lu\n",pthread_self());
printf("The pthread!!ID:%lu\n",tid);
while(1){
printf("This is main pthread!!\n");
sleep(1);
}
return 0;
}
Linux下執行緒操作函式都是庫函式,需要連結動態庫-pthread
thread: 用於接受一個使用者執行緒ID
attr: 使用者設定執行緒屬性,一般置空
start_routine:執行緒的入口函式,執行緒執行的就是這個函式,這個函式退出了,執行緒也就退出了
arg:用於給執行緒入口函式傳遞引數
返回值:成功:0 失敗:errno
其實在每一個task_struct裡邊都有pid、tgid,其中pid執行緒標識,tgid是描述該執行緒屬於哪個執行緒組(程序)的執行緒,這個tgid實際上是該執行緒所處執行緒組的首執行緒的pid,也就是說在一個執行緒組中tgid等於pid的那個執行緒就是主執行緒。
- 檢視程序中所有執行緒資訊1:ps -efL
ps -efL |head -1&&ps -efL |grep create
LWP:輕量級程序,這一列記錄的就是每個執行緒task_struct中的tid
NLWP:這個程序中有幾個執行緒
PPID:父程序ID
PID:程序ID(執行緒組tgid),也就是主執行緒的tid。
2、檢視執行緒:
ps -aL |head -1&&ps -aL |grep create
PID:程序ID(執行緒組tgid),也就是主執行緒的tid。
LWP:執行緒ID,各個task_struct中的tid.
執行緒沒有父子之分,所有執行緒都是有平級的,如果非要說有區別的話,那麼就是主執行緒和其他執行緒的區別。
執行緒終止
執行緒的退出方式:
- return num;退出:在main函式中呼叫return,效果是退出程序,線上程中呼叫return,退出執行緒。
- exit(num)針對整個程序,即使在非主執行緒中使用exit(),也會是整個程序退出.
void pthread_exit(void *retval);
Compile and link with -pthread.
DESCRIPTION
The pthread_exit() function terminates the calling thread and returns a value
via retval that (if the thread is joinable)is available to another thread in
the same process that calls pthread_join(3).
retval儲存執行緒退出狀態資訊,如果不關心,可以置空。
終止一個呼叫執行緒,執行緒呼叫執行緒退出,main函式呼叫,主執行緒退出,其他執行緒成了殭屍執行緒,程序顯示殭屍程序,程序顯示的是主執行緒stat。終止一個呼叫執行緒,執行緒呼叫執行緒退出,main函式呼叫,主執行緒退出,其他執行緒成了殭屍執行緒,程序顯示殭屍程序,程序顯示的是主執行緒stat。
檢視執行緒的命令
ps aux -L | head -1 && ps aux -L | grep exit | grep -v 'test'
ps -aL
int pthread_cancel(pthread_t thread);
Compile and link with -pthread.
主執行緒用來取消主執行緒自己的執行緒id為thread的執行緒
//這段程式碼用於演示執行緒退出的幾種方式
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<stdlib.h>
void *thr_start(void *arg)
{
//線上程中呼叫exit函式會怎樣?
//程序要是退出了,那麼程序中的執行緒也會退出
//exit(0);
//return NULL;
//sleep(5);
//pthread_exit(NULL);
while(1){
printf("child pthread!!!\n");
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = -1;
ret = pthread_create(&tid,NULL,thr_start,NULL);
if(ret != 0){
printf("pthread create error\n");
return -1;
}
//int pthread_cancel(pthread_t thread);
//取消普通執行緒
pthread_cancel(tid);
while(1){
printf("first pthread!!\n");
sleep(1);
}
return 0;
}
執行緒等待(執行緒分離)
主執行緒退出,其他執行緒也會形成殭屍執行緒:佔用了一部分資源不釋放,最終造成資源洩露。
執行緒等待就是接受執行緒的返回值,然後釋放執行緒的所有資源。
執行緒處於joinable狀態才能被等待,如果一個執行緒在呼叫pthread_join函式之前已經退出,則pthread_join函式立即返回,否則阻塞等待,直到這個指定的執行緒退出,才會返回。
pthread_join函式的返回值只需要考慮執行緒的退出碼或者errorno,不需要考慮執行緒異常的情況,因為一旦執行緒產生異常,系統會認為整個程序異常,這個程序就掛了。
int pthread_join(pthread_t thread, void **retval); //避免資源洩露,執行緒屬性是joinable狀態
pthread_t thread:指定等待執行緒的執行緒ID
void **retval:將執行緒退出資訊接收到retval中
The pthread_join() function waits for the thread specified by thread to terminate. If that thread has already terminated,
then pthread_join() returns immediately. The thread specified by thread must be joinable.
//演示執行緒退出等待,獲取執行緒返回值
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
//執行緒需要被等待的條件是:執行緒處於joinable狀態,一個執行緒創建出來預設屬性就是joinable狀態
void* Func()
{
sleep(2);//讓這個非主執行緒睡覺,讓主執行緒等它睡醒後,接收該執行緒的退出狀態值,釋放資源
printf("I am not the main pthread\n");
pthread_exit("I am not the main pthread !I will exit!");
return "I am the best!!";
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid,NULL,Func,NULL);
if(ret!=0){
perror("pthread_create error");
exit(-1);
}
printf("I am the Main pthread\n");
char* addr;//定義一級指標用於給pthread_join的第二個引數初始化,否則二級指標為空,不能使用
char** ptr = &addr;
sleep(5);
pthread_join(tid,(void**)ptr);//主執行緒到這裡會阻塞等待它建立的執行緒id是tid的執行緒
printf("%s\n",*ptr);
//pthread_cancel(tid);
//sleep(5);
//pthread_join(tid,(void**)ptr);//主執行緒到這裡會阻塞等待它建立的執行緒id是tid的執行緒
//如果一個執行緒是被取消的那麼它的返回值只有一個-1,PTHREAD_CANCELD
//printf("%d\n",addr);
return 0;
}
執行緒分離:功能是設定狀態
我們等待一個執行緒是因為需要獲取執行緒的返回值,並且釋放資源。那麼假如我不關心返回值,那麼這個等待將毫無意義,僅僅是為了釋放資源。因此就有一個執行緒屬性叫:執行緒分離屬性 detach屬性,這個屬性就是需要設定,它是告訴作業系統,這個指定的執行緒我不關心返回值,所以如果執行緒退出了,作業系統就自動把所有資源回收。而設定一個執行緒分離屬性我們常稱為執行緒分離
執行緒的detach屬性與joinable屬性相對應,也相沖突,兩者不會同時存在。如果一個執行緒屬性是detach,那麼pthread_join的時候將直接報錯,所以我們說,只有一個執行緒處於joinable狀態才可以被等待
執行緒被設定成detach屬性,退出後將自動釋放資源,不會形成殭屍執行緒
detach與joinable屬性相沖突,無法同時存在,設定detach就表明了不關心返回值
int pthread_detach(pthread_t thread);
執行緒也可以分離自己pthread_detach(pthread_self());
- 使用gdb除錯的注意事項
1、編譯過程一定要加-g選項:因為在Linux系統下,預設生成的是release(不加除錯資訊)版本的可執行程式,如果不加-g,則不能除錯。例如編譯hello.c生成hello的debug版本;
gcc -g hello.c -o hello
2、在開啟gdb除錯不想看到那麼一大堆版本資訊可以加-q,例如除錯hello
gdb -q hello