文章目錄
- 線程概念
- Linux中線程是否存在的討論
- 線程創建和線程控制
- 線程的終止和等待(三種終止方式 + pthread_join()的void**retval)
線程概念
線程就是進程內部的一個執行流,線程在進程內運行,線程在進程的地址空間內運行,擁有該進程的一部分資源。
這句話一說可能老鐵們直接蒙蔽,線程就線程嘛,怎么還在進程里面運行呢?還在地址空間內運行?而且擁有進程的一部分資源,這都是什么鬼?
如何看待線程在地址空間內運行呢?實際進程就像一個封閉的屋子,線程就是在屋子里面的人,而地址空間就是一個個的窗戶,屋子外面就是進程對應的代碼和數據,一個屋子里面當然可以有多個人,而且每個人都可以挑選一個窗戶看看外面的世界。
在上面的例子中,每個人挑選一個窗戶實際就是將進程的資源分配給進程內部的多個執行流,以前fork創建子進程的時候,不就是將父進程的一部分代碼塊兒交給子進程運行嗎?子進程不就是一個執行流嗎?
而今天我們所談到的線程道理也是類似,我們可以將進程的資源劃分給不同的線程,讓線程來執行某些代碼塊兒,而線程就是進程內部的一個執行流。那么此時我們就可以通過地址空間+頁表的方式將進程的部分資源劃分給每一個線程,那么線程的執行粒度一定比之前的進程更細!
Linux中線程是否存在的討論
我們在思考一下,如果Linux在內核中真的創建出了我們上面所談論到的線程,那么Linux就一定要管理內核中的這些線程,既然是管理,那就需要先描述,再組織,創建出真正的 TCB(Thread Create Block)結構體來描述線程,線程被創建的目的不就是被執行,被CPU調度嗎?既然所有的線程都要被調度,那每個線程都應該有自己獨立的thread_id,獨立的上下文,狀態,優先級,獨立的棧(線程執行進程中的某一個代碼塊兒)等等,那么大家不覺得熟悉嗎?單純從CPU調度的角度來看,線程和進程有太多重疊的地方了!
所以Linux中就沒有創建什么線程TCB結構體,直接復用進程的PCB當作線程的描述結構體,用PCB來當作Linux系統內部的"線程"。這么做的好處是什么呢?如果要創建真正的線程結構體,那就需要對其進行維護,需要和進程構建好關系,每個線程還需要和地址空間進行關聯,CPU調度進程和調度線程還不一樣,操作系統要對內核中大量的進程和線程做管理,這樣維護的成本太高了!不利于系統的穩定性和健壯性,所以直接復用PCB是一個很好的選擇,維護起來的成本很低,因為直接復用原來的數據結構就可以實現線程。所以這也是Linux系統既穩定又高效,成為世界上各大互聯網公司服務器系統選擇的原因。(而windows系統內是真正有對應的TCB結構體的,他確實創建出了真正的線程,所以維護起來的成本就會很高,這也是windows用的用的就卡起來,或者藍屏的原因,因為不好維護啊,實現的結構太復雜!代碼健壯性不高)
在知道linux的線程實現方案之后,我們又該如何理解線程這個概念呢?現在PCB都已經不表示進程了,而是代表線程。以前我們所學的進程概念是:進程的內核數據結構+進程對應的代碼和數據,但今天站在內核視角來看,進程的概念實際可以被重構為:承擔分配系統資源的基本實體!進程分配了哪些系統資源呢?PCB+虛存+頁表+物存。所以進程到底是什么呢?
那在linux中什么是線程呢?線程是CPU調度的基本單位,也就是struct task_struct{},PCB就是線程,為進程中的執行流!
那我們以前學習的進程概念是否和今天學習的進程概念沖突了呢?當然沒有,以前的進程也是承擔分配系統資源的基本實體,只不過原來的進程內部只有一個PCB,也就是只有一個執行流,而今天我們所學的進程內部是有多個執行流,多個PCB!
所以: Linux內核中有沒有真正意義上的線程, Linux用進程的PCB來模擬線程,是完全屬于自己實現的一套方案!
站在CPU的角度來看,每一個PCB,都可以稱之為輕量級進程,因為它只需要PCB即可,而進程承擔分配的資源更多,量級更重!
Linux線程是CPU調度的基本單位,進程是承擔分配系統資源的基本實體!
進程用來整體向操作系統申請資源,線程負責向進程伸手要資源。如果線程向操作系統申請資源,實質上也是進程在向操作系統要資源,因為線程在進程內部運行,是進程內部的一部分!
Linux內核中雖然沒有真正意義上的線程,但雖無進程之名,卻有進程之實!
程序員只認線程,但Linux沒有線程只有輕量級進程,所以Linux無法直接提供創建線程的系統調用接口,只能提供創建輕量級進程的接口!
線程創建和線程控制
#include <iostream>
#include <string>
#include<unistd.h>using namespace std;void *start_routine(void *arg)
{string name = static_cast<const char *>(arg);while (true){cout << "new thread: " << name << endl;sleep(1);}
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, start_routine, (void *)"thread-1");while (true){cout << "main thread" << endl;sleep(1);}return 0;
}
創建一個線程比較簡單沒什么含金量,所以在線程控制這里選擇創建一批線程,來看看多個線程下的進程運行情況。
在線程的錯誤檢查這里,并不會設置全部變量errno,道理也很簡單,線程出錯了,那其實就是進程出錯了,錯誤碼這件事不應該是我線程來搞,這是你進程的事情和我線程有什么關系?所以線程也沒有理由去設置全局變量errno,他的返回值只表示成功或錯誤,具體的返回狀態,其實是要通過pthread_join來獲取的!
創建一批線程也并不困難,我們可以搞一個vector存放創建出來的每個線程的tid,但從打印出來的新線程的編號可以看出來,打印的非常亂,有的編號還沒有顯示,這是為什么呢?(我們主觀認為應該是打印出來0-9編號的線程啊,這怎么打印的這么亂呢?)
其實這里就涉及到線程調度的話題了,創建出來的多個新線程以及主線程誰先運行,這是不確定的,這完全取決于調度器,我們事先無法預知哪個線程先運行,所以就有可能出現,新線程一直沒有被調度,主線程一直被調度的情況,也有可能主線程的for循環執行到i等于6或9或8的時候,新線程又被調度起來了,此時新線程內部就會打印出創建成功的語句。所以打印的結果很亂,這也非常正常,因為哪個線程先被調度是不確定的!
線程的終止和等待(三種終止方式 + pthread_join()的void**retval)
再談完線程的創建之后,那什么時候線程終止呢?所以接下來我們要談論的就是線程終止的話題,線程終止總共有三種方式,分別為return,pthread_exit,pthread_cancel
我們知道線程在創建的時候會執行對應的start_routine函數指針指向的方法,所以最正常的線程終止方式就是等待線程執行完對應的方法之后,線程自動就會退出,如果你想要提前終止線程,可以通過最常見的return的方式來實現,線程函數的返回值為void*,一般情況下,如果不關心線程退出的情況,直接return nullptr即可。
和進程終止類似的是,除return這種方式外,原生線程庫還提供了pthread_exit接口來終止線程,接口的使用方式也非常簡單,只要傳遞一個指針即可,同樣如果你不關心線程的退出結果,那么也只需要傳遞nullptr即可。
#include <iostream>
#include <string>
#include<unistd.h>
#include<vector>
#include <stdio.h>
#include<functional>
#include <time.h>
#include <pthread.h>#define NUM 10
using namespace std;
// using func_t = function<void()>;
typedef function<void()> func_t;class ThreadData
{
public:ThreadData(const std::string &name, const time_t &ctime, func_t f):_name(name), _createtime(ctime), _func_t(f){}public:string _name;time_t _createtime;func_t _func_t;
};void Print()
{std::cout << "我是線程執行的大任務的一部分" << std::endl;
}void* start_routine(void* arg)
{ThreadData* td = static_cast<ThreadData*> (arg);cout << "I am a new thread, my name is : "<< td->_name << " creatname is: " << td->_createtime << endl; td->_func_t();//return nullptr; //線程終止pthread_exit(nullptr);
}int main()
{//創建一批線程for(int i = 0; i < NUM; i++){pthread_t tid;char threadname[64];snprintf(threadname, sizeof(threadname), "%s: %d", "thread" , i+1);//創建一個線程數據對象string tdname = threadname;ThreadData* td = new ThreadData(tdname, (time_t)time(nullptr), Print);pthread_create(&tid, nullptr, start_routine, td);sleep(1);}return 0;
}
談完上面兩種線程終止的話題后,第三種終止方式我們先等會兒再說,與進程類似,進程退出之后要被等待,也就是回收進程的資源,否則會出現僵尸進程,僵尸的這種狀態可以通過ps指令+axj選項看到,同時會產生內存泄露的問題。
線程終止同樣也需要被等待,但線程這里沒有僵尸線程這樣的概念,如果不等待線程同樣也會造成資源泄露,也就是PCB資源未被回收,線程退出的狀態我們是無法看到的,我們只能看到進程的Z狀態。
原生線程庫給我們提供了對應的等待線程的接口,其中join的第二個參數是一個輸出型參數,在join的內部會拿到線程函數的返回值,然后將返回值的內容寫到這個輸出型參數指向的變量里面,也就是寫到我們用戶定義的ret指針變量里,通過這樣的方式來拿到線程函數的返回值。
通過bash的打印結果就可以看到,每個線程都正常的等待成功了。
#include <iostream>
#include <string>
#include<unistd.h>
#include<vector>
#include <stdio.h>
#include<functional>
#include <time.h>
#include <pthread.h>#define NUM 5
using namespace std;
// using func_t = function<void()>;
typedef function<void()> func_t;class ThreadData
{
public:ThreadData(const std::string &name, const time_t &ctime, func_t f):_name(name), _createtime(ctime), _func_t(f){}public:string _name;time_t _createtime;func_t _func_t;
};void Print()
{std::cout << "我是線程執行的大任務的一部分" << std::endl;
}void* start_routine(void* arg)
{ThreadData* td = static_cast<ThreadData*> (arg);cout << "I am a new thread, my name is : "<< td->_name << " creatname is: " << td->_createtime << endl; td->_func_t();//return nullptr; //線程終止//pthread_exit(nullptr);return (void*)110;
}int main()
{vector<pthread_t> tids;//保存線程的tid//創建一批線程for(int i = 0; i < NUM; i++){pthread_t tid;char threadname[64];snprintf(threadname, sizeof(threadname), "%s: %d", "thread" , i+1);//創建一個線程數據對象string tdname = threadname;ThreadData* td = new ThreadData(tdname, (time_t)time(nullptr), Print);pthread_create(&tid, nullptr, start_routine, td);tids.push_back(tid);sleep(1);}void* retval = nullptr;for(int i = 0; i < NUM; i++){//線程的等待pthread_join(tids[i], &retval);cout << "join sucess, retval is: ";cout << (long long)retval << endl;sleep(1);}return 0;
}
在了解join拿到線程函數的返回值之后,我們再來談最后一個線程終止的方式pthread_cancel,叫做線程取消。首先線程要被取消,前提一定得是這個線程是跑起來的,跑起來的過程中,我們可以選擇取消這個線程,換個說法就是中斷這個線程的運行。
如果新線程是被別的線程取消的話,則新線程的返回值是一個宏PTHREAD_CANCELED,這個宏其實就是把-1強轉成指針類型了,所以如果我們join被取消的線程,那join到的返回值就應該是-1,如果線程是正常運行結束退出的話,默認的返回值是0.
我們讓創建出來的每個新線程跑10s,然后在第5s的時候,主線程取消前5個線程,那么這5個線程就會被中斷,主線程阻塞式的join就會提前等待到這5個被取消的線程,并打印出線程函數的返回值,發現結果就是-1,再經過5s之后,其余的5個線程會正常的退出,主線程的join會相應的等待到這5個線程,并打印出默認為0的退出結果。
#include <iostream>
#include <string>
#include <unistd.h>
#include <vector>
#include <stdio.h>
#include <functional>
#include <time.h>
#include <pthread.h>#define NUM 10
using namespace std;
// using func_t = function<void()>;
typedef function<void()> func_t;class ThreadData
{
public:ThreadData(const std::string &name, const time_t &ctime, func_t f): _name(name), _createtime(ctime), _func_t(f){}public:string _name;time_t _createtime;func_t _func_t;
};void Print()
{std::cout << "我是線程執行的大任務的一部分" << std::endl;
}void *start_routine(void *arg)
{ThreadData *td = static_cast<ThreadData *>(arg);int cnt = 10;while (cnt--){cout << "I am a new thread, my name is : " << td->_name << " creatname is: " << td->_createtime << endl;td->_func_t();// return nullptr; //線程終止// pthread_exit(nullptr);// return (void*)110;sleep(1);}
}int main()
{vector<pthread_t> tids; // 保存線程的tid// 創建一批線程for (int i = 0; i < NUM; i++){pthread_t tid;char threadname[64];snprintf(threadname, sizeof(threadname), "%s: %d", "thread", i + 1);// 創建一個線程數據對象string tdname = threadname;ThreadData *td = new ThreadData(tdname, (time_t)time(nullptr), Print);pthread_create(&tid, nullptr, start_routine, td);tids.push_back(tid);//sleep(1);}sleep(5);for (int i = 0; i < NUM / 2; i++){pthread_cancel(tids[i]);cout << "cancel: " << tids[i] << "success" << endl;}void *retval = nullptr;for (int i = 0; i < NUM; i++){// 線程的等待pthread_join(tids[i], &retval);cout << "join sucess, retval is: ";cout << (long long)retval << endl;//sleep(1);}return 0;
}