【Linux】線程概念|線程理解|線程控制

文章目錄

  • 線程概念
  • 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;
}

在這里插入圖片描述
在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/717990.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/717990.shtml
英文地址,請注明出處:http://en.pswp.cn/news/717990.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

LeetCode-第14題-最長公共前綴

1.題目描述 編寫一個函數來查找字符串數組中的最長公共前綴。 如果不存在公共前綴&#xff0c;返回空字符串 ""。 2.樣例描述 3.思路描述 按字符串數組每個數組的長度&#xff0c;將字符串數組從小到大排序&#xff1b;他們的公共前綴一定小于或等于最長元素長度…

(Aliyun AI ACP 06)視覺智能基礎知識:視覺智能常用模型與算法

文章目錄 阿里云人工智能工程師ACP認證考試知識點輔助閱讀&#xff08;Aliyun AI ACP 06&#xff09;視覺智能基礎知識&#xff1a;視覺智能常用模型與算法視覺智能建模流程圖像預處理技術圖像特征提取算法深度學習模型 阿里云人工智能工程師ACP認證考試知識點輔助閱讀 &#…

2024年智能駕駛年度策略:自動駕駛開始由創造型行業轉向工程型行業

感知模塊技術路徑已趨于收斂&#xff0c;自動駕駛從創造型行業邁向工程型行業。在特斯拉的引領下&#xff0c;國內主機廠2022年以來紛紛跟隨特斯拉相繼提出“重感知、輕地圖”技術方案&#xff0c;全球自動駕駛行業感知模塊技術路徑從百花齊放開始走向收斂。我們認為主機廠智能…

2023.3.3周報

目錄 摘要 一、文獻閱讀 1、題目 2、摘要 3、模型架構 4、文獻解讀 一、Introduction 二、實驗 三、結論 二、PINN 一、PINN比傳統數值方法有哪些優勢 二、PINN方法 三、正問題與反問題 三、PINN實驗 一、數學方程 二、模型搭建 總結 摘要 本周我閱讀了一篇…

Postman上傳文件的操作方法

前言 調用某個接口&#xff0c;測試上傳文件功能。一時間不知如何上傳文件&#xff0c;本文做個操作記錄&#xff0c;期望與你有益。 步驟一、設置Headers key:Content-Type value:multipart/form-data 步驟二、設置Body 選擇form-data key:file下拉框選擇file類型value&…

STM32(8)NVIC編程

中斷源由部分片上外設產生 在misc.h中找&#xff0c;雜項 配置NVIC GPIO和AFIO不能產生中斷源&#xff0c;但能通過EXTI&#xff0c;由EXTI產生中斷源 NVIC不需要開啟時鐘&#xff0c;因為NVIC模塊位于內核內部&#xff0c;芯片一上電就能工作。 中斷響應函數 中斷向量表在啟…

Java:JVM基礎

文章目錄 參考JVM內存區域程序計數器虛擬機棧本地方法棧堆方法區符號引用與直接引用運行時常量池字符串常量池直接內存 參考 JavaGuide JVM內存區域 程序計數器 程序計數器是一塊較小的內存空間&#xff0c;可以看做是當前線程所執行的字節碼的行號指示器&#xff0c;各線程…

Unity 常用的4種燈光、制作鏡子、燈光的調用修改數值、

創建燈光時&#xff0c;一般用4種&#xff1a;定向光、點光源、聚光、區域光、 定向光&#xff1a;太陽 點光源&#xff1a;燈泡 聚光燈&#xff1a;手電筒 區域光&#xff1a;烘焙-貼圖 燈光選擇已烘焙 需要先選擇被烘焙的物體&#xff0c;然后再選擇Contribute GI 等待進…

java中的set

Set Set集合概述和特點 不可以存儲重復元素 沒有索引,不能使用普通for循環遍歷 哈希值 哈希值簡介 是JDK根據對象的地址或者字符串或者數字算出來的int類型的數值 如何獲取哈希值 Object類中的public int hashCode()&#xff1a;返回對象的哈希碼值。 哈希值的特點 同一個…

分布式ID生成算法|雪花算法 Snowflake | Go實現

寫在前面 在分布式領域中&#xff0c;不可避免的需要生成一個全局唯一ID。而在近幾年的發展中有許多分布式ID生成算法&#xff0c;比較經典的就是 Twitter 的雪花算法(Snowflake Algorithm)。當然國內也有美團的基于snowflake改進的Leaf算法。那么今天我們就來介紹一下雪花算法…

計算機視覺基礎知識(二)---數字圖像

像素 像素是分辨率的單位;構成位圖圖像的最基本單元;每個像素都有自己的顏色; 圖像分辨率 單位英寸內的像素點數;單位為PPI(Pixels Per Inch),為像素每英寸;PPI表示每英寸對角線上所擁有的像素數目:,x:長度像素數目,y:寬度像素數目,Z:屏幕大小;屏幕尺寸(大小)指的是對角線長…

GO語言學習筆記(與Java的比較學習)(八)

接口與反射 接口是什么 Go 語言不是一種 “傳統” 的面向對象編程語言&#xff1a;它里面沒有類和繼承的概念。 但是 Go 語言里有非常靈活的 接口 概念&#xff0c;通過它可以實現很多面向對象的特性。接口提供了一種方式來 說明 對象的行為&#xff1a;如果誰能搞定這件事&…

springer模板參考文獻不顯示

Spring期刊模板網站&#xff0c;我的問題是23年12月的版本 https://www.springernature.com/gp/authors/campaigns/latex-author-support/see-where-our-services-will-take-you/18782940 參考文獻顯示問好&#xff0c;在sn-article.tex文件中&#xff0c;這個sn-mathphys-num…

數據結構c版(3)——排序算法

本章我們來學習一下數據結構的排序算法&#xff01; 目錄 1.排序的概念及其運用 1.1排序的概念 1.2 常見的排序算法 2.常見排序算法的實現 2.1 插入排序 2.1.1基本思想&#xff1a; 2.1.2直接插入排序&#xff1a; 2.1.3 希爾排序( 縮小增量排序 ) 2.2 選擇排序 2.2…

rtt的io設備框架面向對象學習-io設備管理層

目錄 1.設備基類2.rtt基類2.1 rtt基類定義2.2 對象容器定義2.3 rtt基類構造函數 3.io設備管理接口4.總結 這層我的理解就是rtt基類和設備基類所在&#xff0c;所以抽離出來好點&#xff0c;不然每個設備類都要重復它。 1.設備基類 /include/rtdef.h中定義了設備基類struct rt_…

記錄踩過的坑-PyTorch

安裝報錯 按PyTorch官網給出的命令 pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 報錯 ERROR: Could not find a version that satisfies the requirement torch (from versions: none) ERROR: No matching distributio…

Redis為什么這么快?

基于內存&#xff1a;Redis 將數據存儲在內存中&#xff0c;內存訪問速度遠高于磁盤訪問速度&#xff0c;因此能夠快速讀寫數據。單線程模型&#xff1a;Redis 使用單線程模型來處理客戶端請求&#xff0c;避免了多線程之間的切換開銷&#xff0c;簡化了并發控制&#xff0c;提…

STM32(11)按鍵產生中斷

1.初始化IO引腳&#xff0c;設置模式&#xff0c;速度等 2.設置AFIO&#xff08;配置EXTI的引腳映射&#xff09;&#xff0c;記得開啟時鐘 3.配置EXTI的通道&#xff08;EXTI0和EXTI1&#xff09; 4.配置NVIC 4.1 中斷優先級分組 4.2 配置中斷 5.編寫中斷響應函數 在中斷向量…

消息隊列的實現

8.8 消息隊列 隊列是一種先進先出的結構&#xff0c;消息隊列是進程(線程)常用的一種方法&#xff0c;實現消息隊列常用的方法&#xff1a; &#xff08;1&#xff09;阻塞隊列 &#xff08;2&#xff09;無鎖隊列 &#xff08;3&#xff09;環形隊列 值得注意的是&#xff…

藍橋ACM培訓-實戰1

前言&#xff1a; 今天老師沒講課&#xff0c;只讓我們做了一下幾道題目。 正文&#xff1a; Problem:A 小藍與操作序列&#xff1a; #include<bits/stdc.h> using namespace std; stack<int> a; int main(){int n,flag1,ans;string cz;cin>>n;for(int i1;…