了解了線程的基本原理之后,我們來學習線程在C語言官方庫中的寫法與用法。
1. 常見pthread接口及其背后邏輯
1.1 pthread_create
與線程有關的函數構成了?個完整的系列,絕?多數函數的名字都是以“pthread_”打頭的?要使?這些函數庫, 要通過引?頭文件?<pthread.h>?鏈接這些線程函數庫時要使?編譯器命令的“-l pthread”選項![]()
???pthread_create
函數并不是Linux提供的創建線程的系統調用,而是Linux中對輕量級進程已有的接口進行封裝的動態庫中的函數
#include <iostream>
#include <pthread.h>
#include <unistd.h>using namespace std;void *routine(void *arg)
{//值得注意的是,線程執行的函數的返回值一定是一個void*,用于返回一個指針-》這樣的目的是讓所有類型的參數都能被返回//包括但不限于類對象,函數,容器等等。//而參數void*,是一種泛型編程的實現方式,這樣一來可以根據傳入的內容來指定完成任務。while (true){cout << "new thread" << endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, run, (void *)"thread-1");while (true){cout << "main thread" << endl;sleep(1);}return 0;
}
?由實驗結果可知,main和new thread的執行順序并不固定。
?????????兩個線程是異步打印(使用)到同一個文件資源(顯示器)上,所以存在打印錯亂的情況,但是的確可以做到每一個線程完成自己的任務而不會受到其他線程的干擾,這兩個線程就是兩個不同的執行流,而因為
routine
函數始終在訪問同一個資源,所以此時其是不可重入函數。
pthread_create的底層其實是clone-------一個非常復雜不便于使用的函數。
由于Linux在內核代碼中并沒有將進程和線程完全分成兩個系統管理,所以clone的flag參數就有以下兩種選擇,創建一個parent(進程),或者是只創建一個輕量級進程(clone_thread)
創建進程還是創建線程?一個文字游戲而已。
了解:clone第一個參數,即傳給該線程的函數,第二個是棧空間(那么多虛擬內存的段,為什么要單獨傳一個這?),最后一個是線程要執行的函數的參數。。。。。。。
所以,現在就可以進一步理解用戶級線程
????????
pthread_create調用的是clone,clone其實也是被封裝的,底層還有如do_clone等函數。
最后,關于pthread_create的返回值:
????????pthreads函數出錯時不會設置全局變量errno(??部分其他POSIX函數會這樣做)。?是將錯 誤代碼通過返回值返回。沒有出錯返回0,出錯返回錯誤碼。
??????? ?對于pthreads函數的錯誤,建議通過返回值判定,因為讀取返回值要?讀取線程內的errno變量的開銷更小。(strerror和errno都是之前介紹進程的時候提及的概念)
1.2?pthread_self
pthread_self用于查看任意線程的線程ID
在上一文中,我們提到以下指令,用于查看:
????????ps -aL | head -1 && ps -aL | grep thread
或者
while :; do ps ajx |head -1 && ps ajx | grep test | grep -v grep;sleep 1;echo"------------------------------------------------";done
這個線程的tid值似乎與之前提到的lwp不太一樣呢???
請記住這個伏筆,會在之后的內容中重點聊。
如果覺得這個數字太長,我們可以:
string ToHex(pthread_t tid)
{char buffer[64];snprintf(buffer,sizeof(buffer),"0x%lx",tid);return buffer;
}
這個tid似乎看著很像地址?????????
多線程狀態對于資源的使用
能夠使用以上兩個接口,我們便能創建一些多線程的demo。
下面我們來驗證一下多線程的棧區是否是獨立的:
????????
//..................... void *run(void *arg) {//值得注意的是,線程執行的函數的返回值一定是一個void*,用于返回一個指針-》這樣的目的是讓所有類型的參數都能被返回//包括但不限于類對象,函數,容器等等。//而參數void*,是一種泛型編程的實現方式,這樣一來可以根據傳入的內容來指定完成任務。string name = static_cast<const char*>(arg);int val = 0;while (true){cout << "new thread,name : "<<name << " my_threadID : "<< ToHex(pthread_self())<<"my val : "<<val<<endl;val++;if(name=="thread-1") val++;sleep(1);} }int main() {pthread_t tid1;pthread_t tid2;pthread_create(&tid1, nullptr, run, (void *)"thread-1");pthread_create(&tid2, nullptr, run, (void *)"thread-2"); //...........
實驗現象也不出所料:兩個棧的val值互不影響。
兩個新線程都在修改
val
,但是二者并沒有影響到另外的val
,這也驗證了每個線程實際上也有自己的棧。再嘗試讓兩個線程都修改同一個全局變量:
果然,兩個線程在某種意義上已經形成了“通信”。
????????將
val
變量作為了全局變量供兩個線程進行訪問,此時兩個線程均看得到這個變量,所以這個變量就是兩個線程的共享資源,與前面顯示器打印問題一樣,因為共用并且沒加保護,導致一個線程的修改會影響到另一個線程,這個效果同樣適用于靜態變量
兩個線程誰先執行不一定,并且不同的線程會瓜分時間片
其實,獨立的線程不代表是私有的線程,強行使用地址去訪問某一個線程棧的內容依然能訪問到。?
結果:
? ? ? ? ? ? ? ? ? ? ??
但是這樣的做法是非常不推薦的,是錯誤的。也在一定的程度上的說明,只要拿到虛擬地址就能訪問,一切的“不允許”都是被規則規定的。
?
線程局部存儲?
如果希望一個變量被每一個線程都單獨創建副本,則使用__thread來修飾該變量。該關鍵詞只能用于修飾內置類型。
棧的特性與存儲
棧是每個線程私有的內存區域,用于存儲局部變量、方法參數和返回地址等。每個新線程都會創建自己的棧。棧上的數據獨立于其他線程,這意味著如果一個線程在其棧中創建了一個變量,其他線程不能直接訪問該變量,除非通過方法參數或共享對象顯式傳遞。這種獨立性提供了天然的線程安全,因為每個線程都有自己的棧空間,不會無意間影響其他線程的數據。
堆的特性與存儲
堆是一個全局共享的內存區域,用于動態分配內存。所有線程都可以訪問堆上分配的對象。如果一個對象在堆上被創建,并且其引用(內存地址)被傳遞給多個線程,那么這些線程就可以同時訪問甚至修改該對象的內容。由于堆是共享的,所以在多線程環境下訪問堆上的對象時需要特別小心,通常需要使用同步機制來避免競態條件或數據不一致的問題。
總之,不論是堆上的數據還是棧上的數據,只要能獲取到指定內容的地址,就不存在所謂的棧獨立性,此刻棧和堆都是共享的,而所謂的棧獨立性和堆共享性更強調所有的線程都有自己的棧,但是堆共有一個
1.3 pthread_join
????????已經退出的線程,其空間沒有被釋放,仍然在進程的地址空間內。???????創建新的線程不會也不能復?剛才退出線程的地址空間。? ? ? ? 為了解決類似于進程中“僵尸進程”的問題,并且獲得線程的任務執行情況。被主線程創建的線程一般需要被等待并且回收。
參數 :thread: 線程 ID(希望被join掉,也就是希望被終止的線程ID)value_ptr: 它指向?個指針(雙重指針),是線程執行的函數的返回值的地址。返回值:成功返回 0 ;失敗返回錯誤碼。
?比如我們現在不需要線程執行的任務的返回值:
pthread_join的第二個參數是一個輸出型參數,因為線程執行的函數是一個void*類型的返回值,為了讓這樣的void*參數能夠自動適應所有的返回值,所以必須使用一個void**的參數
?線程執行的任務想返回一個10,所以就用void*強轉一下10(ret是一個void*,用于接受這個10的值,所以10必須強轉成void*)。
?void*是8個字節(現在的linux機器或者其他機器幾乎都是64位),此時10被當成了一個void*
???????????????????????????????????????????????????????
所以用int去強轉會出現報錯,應該采用long long去強轉。
或者用一個堆指針來完成這個任務也可以,先把void*的指針變成int*,再解引用。
一個線程只要return,就退出了
一個void**類型的變量ret去記錄線程執行的函數的返回值(return的值)
錯誤使用:
這么做雖然能正確找到這個ret值,但是是不科學的,因為a是一個棧上的變量,按理來說這個變量會銷毀。更建議使用堆指針。
以上代碼說明說明堆空間是共享的。線程之間能拿到其他線程創建的堆資源,不過后續需要自己銷毀資源
另外,直接pthread_join自己會報錯:
? ??
既然pthread_create的回調函數支持傳入任何參數,那么假設我們希望給每一個線程傳一個結構體,并且打印、操作該結構體對應的數據。
一個簡單的傳結構體指針demo:
class ThreadData
{
public:ThreadData(const string &name, int a, int b): _name(name), _dataA(a), _dataB(b){}pair<int, int> GetInfo(){return pair<int, int>(_dataA, _dataB);}const int GetA(){return _dataA;}void Exec(){_dataA++;_dataB *= 10;cout << _name << endl;}~ThreadData() {}private:string _name;int _dataA;int _dataB;
};void *routine(void *th_data)
{ThreadData *p_data = (ThreadData *)th_data;//while (true)//{cout << "new thread,myID: " << ToHex(pthread_self()) << " A:B " <<p_data->GetInfo().first << " : " << p_data->GetInfo().second << endl;p_data->Exec();cout << "new thread,myID: " << ToHex(pthread_self()) << " A:B " << p_data->GetInfo().first << " : " << p_data->GetInfo().second << endl;sleep(1);//}return (void*)p_data;
}int main()
{pthread_t tid;ThreadData *th_p = new ThreadData("thread-1", 10, 20);int err_n1 = pthread_create(&tid,nullptr,routine,(void*)th_p);if(err_n1!=0){cerr<<"create error "<<strerror(err_n1)<<endl;return 1;}else {cout<<"create success"<<endl;}void* ret = nullptr;int err_n2 = pthread_join(tid,&ret);if (err_n2 != 0){cerr << "join error" << strerror(err_n2) << endl;}elsecout << "joined success! " << "ret num A : " << ((ThreadData*)ret)->GetA() << endl;
}
創建多個線程的demo:
之前的ThreaData就是為了在一個新的線程的時候有對應的數據和方法? ?????????? ?
??pthread_join在進行的其實也是一種阻塞等待,這會影響主線程的工作效率。
1.4 線程分離pthread_detach
????????默認情況下,新創建的線程是joinable的,線程退出后,需要對其進?pthread_join操作,否則?法釋放資源,從?造成系統泄漏。?????????如果不關?線程的返回值,join是?種負擔,這個時候,我們可以告訴系統,當線程退出時,?動釋放線程資源
join和detach是矛盾的。
????????需要注意的是,在多執行流的情況下,一定要確保主執行流最后退出,防止主執行流先退出導致?分離的線程?需要的資源被釋放導致錯誤。線程可以自主分離(在線程的執行函數中執行pthread_detach(pthread_self()); 或者被迫分離-》主線程等其他線程調用該函數。
線程程序替換
????????注意,進程程序替換是不可以直接發生在線程執行流中的,因為程序替換的本質是替換掉當前進程的代碼,此時線程的代碼也會被替換從而導致錯誤,但是可以在線程中創建子進程,再在子進程中使用進程程序替換
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ????????
1.5 線程退出
首先,不能直接用exit(),因為exit()會直接讓整個進程都退出。
pthread_exit(),pthread_exit的參數也會被pthread_join給拿到----------->因為pthread_join本來就是拿的return的值,現在由pthread_exit完成這個“return”的任務。
pthread_cancel
pthread_cancel
:主要用于主線程去取消其他進程。(類似于父進程去取消子進程的過程)
但是不建議直接使用,因為不清楚被取消的線程的工作狀態
一個被pthread_cancle取消的線程的join值是-1(ret 返回的值)
這個接口要慎用,如果在新線程未完成任務就被主線程結束,主線程可能也會結束。
并且主線程在cancle的時候可能不清楚新線程的狀態,有可能出錯。?
一個被pthread_cancle取消的線程的join值是-1(ret? ? )
2. CPP的封裝
除了C語言對系統的相關線程操作進行了封裝外,其他編程語言也會做同樣的事情,這樣可以確保語言具有可移植性,同樣,C++中也有對應的線程庫<thread>,具體接口見C++線程庫部分,但是需要注意的是,如果是在Linux下使用C++線程庫編寫代碼,編譯該代碼時依舊需要攜帶-lpthread,因為C++線程庫本質也是封裝了libpthread.so
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 錯誤:? ?
lambda表達式構造thread:
? ?