?感謝您閱讀本篇文章,文章內容是個人學習筆記的整理,如果哪里有誤的話還請您指正噢?
? 個人主頁:余輝zmh–CSDN博客
? 文章所屬專欄:Linux篇–CSDN博客
文章目錄
- 一.線程創建
- 二.線程等待
- 三.線程終止
- 四.擴展內容
- 1.重談`pthread_create`函數
- 2.C++11線程庫
- 3.線程棧結構
- 4.線程局部存儲
- 5.分離線程
POSIX線程庫:
- 與線程有關的函數構成了一個完整的系列,絕大多數函數的名字都是以
pthread_
開頭的; - 要使用這些線程的相關函數,需通過引入頭文件
<pthread.h>
; - 鏈接這些線程函數庫時要使用編譯器命令的
-lpthread
選項。
一.線程創建
線程創建函數:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
功能:創建一個新的線程
參數:
thread
:返回線程的ID;attr
:設置線程的屬性,為空時(nullptr)表示使用默認屬性;start_routine
:函數地址,線程啟動后要執行的函數arg
:傳入線程啟動函數的參數
返回值:成功返回0;失敗返回錯誤碼
測試代碼:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;// 新線程的執行函數
void *pthreadRoution(void *args){while(true){cout << "new thread, pid: " << getpid() << endl;sleep(1);}
}int main(){pthread_t tid;// 主線程創建一個新線程pthread_create(&tid, nullptr, pthreadRoution, nullptr);// 主線程while(true){cout << "main thread, pid: " << getpid() << endl;sleep(1);}return 0;
}
結合創建的線程重新認識一下線程的相關概念:
1.任何一個線程被干掉,其余線程包括整個進程都會被干掉,所以這就是為什么線程的健壯性很差。
2.在多線程情況下,一個方法可以被多個執行流同時執行,這種情況就是show
函數被重入了。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;// show函數方法
void show(const string &name){cout << name << "say# " << "hello thread" << endl;
}// 新線程的執行函數
void *pthreadRoution(void *args){while(true){//cout << "new thread, pid: " << getpid() << endl;show("[new thread]");sleep(2);}
}int main(){pthread_t tid;// 主線程創建一個新線程pthread_create(&tid, nullptr, pthreadRoution, nullptr);// 主線程while(true){//cout << "main thread, pid: " << getpid() << endl;show("[main thread]");sleep(2);}return 0;
}
3.未初始化和已初始化的全局變量在所有線程中是共享的:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;int g_val = 100;
// 新線程的執行函數
void *pthreadRoution(void *args){while(true){printf("new thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);sleep(2);}
}int main(){pthread_t tid;// 主線程創建一個新線程pthread_create(&tid, nullptr, pthreadRoution, nullptr);// 主線程while(true){printf("main thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);sleep(2);g_val++;}return 0;
}
根據上面的例子可以發現,線程之前想要通信會變得非常簡單,因為線程之間天然的就具有共享資源。
二.線程等待
一般而言,主線程一定會是最后退出的,因為其他線程是由主線程創建的,主線程就要對創建出來的新線程做管理,和父進程等待回收子進程一樣,主線程也要等待其他線程進行回收,否則就會造成類似于僵尸進程的問題,比如內存泄漏;同理,主線程創建新線程肯定是要執行一些任務,最后新線程的執行情況也是要返回給主線程的。
所以線程等待和進程等待同理,兩個目的:
1.防止新線程造成內存泄露(主要目的)
2.如果需要,主線程也可以獲取新線程的執行結果
線程等待函數:
#include <pthread.h>int pthread_join(pthread_t thread, void **retval);
參數:
thread
:線程IDvalue_prt
:二級指針,指向一個指針的地址,輸出型參數,用來獲取線程的返回值;如果不關心返回值,可以直接設置為空指針
返回值:成功返回0;失敗返回錯誤碼。
調用該函數的線程將掛起等待,直到ID為thread的線程終止;一般都是主線程調用,所以主線程等待時默認是阻塞式等待。
測試代碼:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;int g_val = 100;
// 新線程的執行函數
void *pthreadRoution(void *args){int cnt = 5;while (true){printf("new thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);sleep(2);cnt--;if (cnt == 0){break;}}return (void *)100;
}int main(){pthread_t tid;// 主線程創建一個新線程pthread_create(&tid, nullptr, pthreadRoution, nullptr);void *retval;pthread_join(tid, &retval);cout << "main thread quit ..., ret: " << (long long int)retval << endl;return 0;
}
為什么線程等待時不用考慮異常呢?
因為根本做不到,一旦其中一個線程出現異常,整個進程也就直接終止退出了。異常問題是由進程考慮的,線程只需要考慮正常情況即可。
三.線程終止
線程終止時直接使用return
語句返回是其中一種方法,除了這個還用其他方法,
先測試使用exit
終止線程:
//線程等待的測試代碼中使用exit終止退出
void *pthreadRoution(void *args){int cnt = 5;while (true){printf("new thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);sleep(1);cnt--;if (cnt == 0){break;}}exit(11);//return (void *)100;
}
最后的結果現象就是,新線程終止退出后,主線程并沒有回收新線程,這是因為調用exit
函數使整個進程都終止退出了。
任何一個線程調用exit,都表示整個進程終止;exit
是用來終止進程的,不能用來終止線程。
線程終止可以使用線程庫中的pthread_exit
函數
線程終止函數:
void pthread_exit(void *value_ptr);
參數:value_ptr
:指向線程終止時返回值的地址;注意,要返回的指針不能指向一個局部變量
返回值:無返回值,線程結束的時候無法返回到他的調用者。
測試代碼:
void *pthreadRoution(void *args){int cnt = 5;while (true){printf("new thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);sleep(1);cnt--;if (cnt == 0){break;}}pthread_exit((void *)100);// exit(11);// return (void *)100;
}
如果主線程先退出,創建出的新線程后退出,最后的現象就是一旦主線程,其余的線程都會退出,也就是整個進程退出:
int main(){pthread_t tid;// 主線程創建一個新線程pthread_create(&tid, nullptr, pthreadRoution, nullptr);// 主線程一秒后退出sleep(1);return 0;void *retval;pthread_join(tid, &retval);cout << "main thread quit ..., ret: " << (long long int)retval << endl;return 0;
}
因為主線程是在main函數中,在main函數return 相當于調用exit函數終止整個進程;
需要注意的是,使用pthread_exit
或者return
這兩種方式來終止線程時,返回的指針所指向的內存單元必須是全局的或者是在堆區上分配的,不能在線程當前執行的函數的棧上分配,因為一旦線程結束函數調用時,棧上分配的空間就會自動釋放。
一個線程終止退出,除了上面的的return
和pthread_exit
函數兩種方式以外,還有一種退出方式:線程取消,調用pthread_cancel
函數
int pthread_cancel(pthread_t thread);
參數:thread
:線程ID
返回值:成功返回0;失敗返回錯誤碼
線程取消是由主線程調用pthread_cancel
函數像目標線程發送一個終止請求,測試代碼:
int main(){pthread_t tid;// 主線程創建一個新線程pthread_create(&tid, nullptr, pthreadRoution, nullptr);sleep(1);pthread_cancel(tid);void *retval;pthread_join(tid, &retval);cout << "main thread quit ..., ret: " << (long long int)retval << endl;return 0;
}
線程取消后,退出結果就會設置成一個宏PTHREAD_CANCELED
(表示-1),線程等待就會獲取到退出結果-1。
總結:如果需要只終止某個線程而不終止整個進程,有三種方式:
1.線程執行的函數return;主線程不適用。
2.線程調用pthread_exit終止自己。
3.一個線程可以調用pthread_cancel終止同一進程中的另一個線程。
四.擴展內容
1.重談pthread_create
函數
前面提到過pthread_create
函數的第四個參數和返回值都是void*
類型,采用該類型主要是通過泛型的思想,適配任何指針類型。
其中pthread_create
函數的第四個參數和返回值除了傳遞普通的內置類型(比如整形,字符串型等),還可以傳遞自定義類型(類和對象)
通過一段代碼來測試:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;class Request{
public:Request(int start, int end, const string &threadname):_start(start),_end(end),_threadname(threadname){}
public:int _start;int _end;string _threadname;
};class Response{
public:Response(int result, int exitcode):_result(result),_exitcode(exitcode){}
public:int _result;int _exitcode;
};void *sumcount(void *args){Request *rq = static_cast<Request *>(args);Response *rsp = new Response(0, 0);for (int i = rq->_start; i <= rq->_end;i++){rsp->_result += i;}delete rq;pthread_exit(static_cast<void *>(rsp));
}int main(){pthread_t tid;Request *rq = new Request(1, 100, "thread 1");// 主線程創建一個新線程pthread_create(&tid, nullptr, sumcount, rq);// 主線程回收新線程void *retval;pthread_join(tid, &retval);Response *rsp = static_cast<Response *>(retval);cout << "rsp->result: " << rsp->_result << " rsp->exitcode: " << rsp->_exitcode << endl;delete rsp;return 0;
}
在上面的測試代碼中,傳遞指針時,將自定義類型(rq
)的指針強制轉換為void*
;然后在線程函數中,將void*
強制轉換為原始類型。
自定義類型的對象都是通過malloc
或者new
在堆上開辟空間的。
而堆空間也是被所有線程共享的,但是共享堆空間不等于自動共享數據
- 堆空間的共享性:所有線程共享同一進程的堆內存區域,但堆上的數據必須通過指針來定位,需要明確直到其地址才能訪問;
- 數據的隔離性:雖然所有線程共享堆空間,但是線程之間默認不知道對方在堆中創建了哪些數據對象。
所以給線程的執行函數傳參時,也可以傳入自定義類型的對象的指針。
2.C++11線程庫
這里先簡單的了解即可,之后學C++11時會再重點講解更詳細的使用
C++11的線程庫簡單使用:
#include <iostream>
#include <unistd.h>
#include <string>
#include <thread>
using namespace std;void threadrun(){while(true){cout << "I am a new thread for C++" << endl;sleep(1);}
}int main(){// C++11的線程庫thread t1(threadrun);t1.join();return 0;
}
C++11里的線程庫本質上還是封裝的原生線程庫,所以使用C++11的線程庫編譯時還是需要帶上-lpthread
選項,此外還要帶上-std=c++11
選項
3.線程棧結構
每個線程在運行時都要有自己獨立的棧結構,因為每一個線程都會有自己的調用鏈,也就注定了每一個線程都要有一個調用鏈對應的獨立棧幀結構,這個棧結構會保存任意一個執行流在運行過程中所有的臨時變量,比如壓棧時傳參形成的臨時變量;返回時的返回值以及地址,包括線程自己在函數中定義的臨時變量,所以每個線程都要有自己獨立的棧結構。
其中主線程直接使用地址空間中提供的的棧結構,這就是系統真正意義上的進程。
除了主線程外,其他線程的獨立棧結構,都在共享區,具體來說是在pthread
庫中,每一個線程都有一個線程控制塊tcb(由線程庫維護),線程的棧結構就存儲在tcb中,而tcb的起始地址就是線程的tid
。
上面講解線程創建時函數的第一個參數線程ID,就是這個線程的tid
地址。后續線程的所有操作都是根據這個線程ID來實現的。
線程庫中還提供了pthread_self
函數,可以獲取線程自身的ID:
pthread_t pthread_self(void);
多線程測試棧區獨立:
#include <iostream>
#include <unistd.h>
#include <vector>
#include <string>
#include <pthread.h>
using namespace std;#define NUM 3class threadData{
public:threadData(int number):threadname("thread-"+to_string(number)){}
public:string threadname;
};string toHex(pthread_t tid){char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", tid);return buffer;
}// 所有線程都會調用這個函數
void *threadRoutine(void *args){threadData *td = static_cast<threadData *>(args);int i = 0;// 每個線程都創建一個test_i變量int test_i = 0; while (i < 5){cout << "pid: " << getpid() << ", tid: " << toHex(pthread_self())<< ", threadname: " << td->threadname<< ", test_i: " << test_i << ", &test_i: " << toHex((pthread_t)&test_i) << endl;sleep(1);i++;test_i++;}delete td;return nullptr;
}int main(){// 創建多線程vector<pthread_t> tids;for (int i = 0; i < NUM; i++){pthread_t tid;threadData *td = new threadData(i); // 在堆區創建pthread_create(&tid, nullptr, threadRoutine, td);tids.push_back(tid);sleep(1);}for (int i = 0; i < tids.size(); i++){pthread_join(tids[i], nullptr);}return 0;
}
根據上面的測試就可以證明,每個線程都有自己的獨立棧結構
如果某個線程想訪問另一個線程棧區上的變量,也是可以實現的:
#include <iostream>
#include <unistd.h>
#include <vector>
#include <string>
#include <pthread.h>
using namespace std;#define NUM 3int *p = nullptr;class threadData{
public:threadData(int number):threadname("thread-"+to_string(number)){}
public:string threadname;
};string toHex(pthread_t tid){char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", tid);return buffer;
}// 所有線程都會調用這個函數
void *threadRoutine(void *args){threadData *td = static_cast<threadData *>(args);int i = 0;int test_i = 0; // 該變量在每個線程的棧區創建if(td->threadname=="thread-2"){p = &test_i;}while (i < 5){cout << "pid: " << getpid() << ", tid: " << toHex(pthread_self())<< ", threadname: " << td->threadname<< ", test_i: " << test_i << ", &test_i: " << &test_i << endl;sleep(1);i++;test_i++;}delete td;return nullptr;
}int main(){// 創建多線程vector<pthread_t> tids;for (int i = 0; i < NUM; i++){pthread_t tid;threadData *td = new threadData(i); // 在堆區創建pthread_create(&tid, nullptr, threadRoutine, td);tids.push_back(tid);sleep(1);}sleep(1);cout << "main thread get a local value,val: " << *p << ", &val: " << p << endl;for (int i = 0; i < tids.size(); i++){pthread_join(tids[i], nullptr);}return 0;
}
所有線程本來就共享代碼區,全局變量區,堆區,共享區等,而對于線程獨立的棧結構上的數據,也是可以被其他線程看到并訪問的,所以線程和線程之間,幾乎沒有秘密。
4.線程局部存儲
全局變量可以被所有線程看到并訪問的
#include <iostream>
#include <unistd.h>
#include <vector>
#include <string>
#include <pthread.h>
using namespace std;#define NUM 3//int *p = nullptr;
int g_val = 0;class threadData{
public:threadData(int number):threadname("thread-"+to_string(number)){}
public:string threadname;
};string toHex(pthread_t tid){char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", tid);return buffer;
}// 所有線程都會調用這個函數
void *threadRoutine(void *args){threadData *td = static_cast<threadData *>(args);int i = 0;//int test_i = 0; // 該變量在每個線程的棧區創建// if(td->threadname=="thread-2"){// p = &test_i;// }while (i < 5){cout << "pid: " << getpid() << ", tid: " << toHex(pthread_self())<< ", threadname: " << td->threadname<< ", g_val: " << g_val << ", &g_val: " << &g_val << endl;sleep(1);i++;//test_i++;g_val++;}delete td;return nullptr;
}int main(){// 創建多線程vector<pthread_t> tids;for (int i = 0; i < NUM; i++){pthread_t tid;threadData *td = new threadData(i); // 在堆區創建pthread_create(&tid, nullptr, threadRoutine, td);tids.push_back(tid);sleep(1);}sleep(1);//cout << "main thread get a local value,val: " << *p << ", &val: " << p << endl;for (int i = 0; i < tids.size(); i++){pthread_join(tids[i], nullptr);}return 0;
}
在上面的測試代碼中,g_val
是一個全局變量,被所有線程共享訪問,所以這個g_val
其實就是一個共享資源。
如果線程想要一個私有的全局變量,如何實現?
直接在定義的全局變量之前加__thread
即可:
__thread int g_val=0;
每一個線程都訪問的是同一個全局變量,但是每一個全局變量對于每一個線程來講,都是各自私有一份,這種技術就是線程的局部存儲。
__thread
不是C語言或C++的關鍵字,而是編譯器提供的一個編譯選項,編譯的時候會默認將這個g_val
變量給每一個線程在獨立的棧結構上申請一份空間。
注意點:
__thread
定義線程的局部存儲變量時只能用來定義內置類型,不能用來修飾自定義類型:
5.分離線程
- 默認情況下,新創建的線程是
joinable
的,線程退出后,需要對其進行pthread_join
操作,否則無法釋放資源,從而造成內存泄漏問題; - 但是如果線程等待時,并不關心線程的返回值,此時
join
就是一種負擔,這個時候,我們可以使用pthread_detach
函數分離線程,告訴系統當線程退出時,自動釋放線程資源。
int pthread_detach(pthread_t thread);
可以是線程組內其他線程對目標線程進行分離(比如主線程使某個線程分離),也可以是線程自己分離(在線程的執行函數中調用pthread_detach(pthread_self())
)。
注意:joinable
和分離是沖突的,一個線程不能既是joinable
又是分離的。
pthread_join
和pthread_detach
兩個函數不能對同一個線程使用。
以上就是關于線程控制部分的講解,如果哪里有錯的話,可以在評論區指正,也歡迎大家一起討論學習,如果對你的學習有幫助的話,點點贊關注支持一下吧!!!