Linux線程概念與控制(下)

目錄

前言

2.線程控制

1.驗證理論

2.引入pthread線程庫

3.linux線程控制的接口

3.線程id及進程地址空間布局

4.線程棧


前言

? 本篇是緊接著上一篇的內容,在有了相關線程概念的基礎之上,我們將要學習線程控制相關話題!!

2.線程控制

1.驗證理論

先來驗證一下我們上面的理論

創建線程可用pthread_create函數(不是系統調用)

image-20250610204544065

第一個參數傳pthread_t類型變量來獲取新線程的id;第二個參數為線程屬性(設置為nullptr就可以);第三個參數是傳返回值為void*,參數為void *的函數指針;第四個參數就是想傳遞給第三個參數的指針/參數

image-20250611101127995

然后我們正常鏈接是過不了的,因為這不屬于系統調用,我們需要在鏈接時加上pthread第三方庫名稱才行,因為是第三方庫,所以需要帶上l選項—— -lpthread

image-20250611101918960

image-20250611102311046

使用命令:ps -aL來查看所有線程

image-20250611102852031

其中pid是一樣的,證明這兩線程(兩執行流)屬于同一個進程;TTY表示終端,它們都屬于同一個終端,都往顯示器上打印;而LWP則是light weight process——輕量級進程,所以這兩執行流的輕量級進程號分別是902075和902076,LWP和pid相等的那個是主線程

image-20250611103518261

CPU調度的時候看的是LWP,調度只看輕量級進程,我們之前學的getpid雖然拿的是pid,但是在調度的時候拿的還是lwp,因為單進程的話,pid就是lwp嘛

細節問題:

  1. 關于調度的時間片問題:進程的時間片是等分給不同的線程的,因為時間片也是共享的(不可能說創建一個線程就拷貝一份時間片,那樣如果有惡意程序不斷分裂線程就會導致時間片是一直累加的)

  2. 我們可以驗證一下線程異常的情況

    #include <iostream>
    #include <pthread.h>
    #include <unistd.h>
    using namespace std;
    ?
    void *threadrun(void *args)
    {string name = (const char *)args;while (true){sleep(1);cout << "我是新線程: name: " << name << ",pid: " << getpid() << endl;int a = 10;a /= 0;}return nullptr;
    }
    ?
    int main()
    {pthread_t tid;pthread_create(&tid, nullptr, threadrun, (void *)"thread-1");while (true) // 主線程往這里執行,新線程轉而去執行我們的threadrun了{cout << "我是主線程..." << ",pid: " << getpid() << endl;sleep(1);}return 0;
    }

    image-20250611111123815

可以看到當新線程發生異常之后,系統終止的是整個進程——任何一個線程崩潰,都會導致整個進程崩潰,一個崩潰會影響其他人,所以健壯性低

  1. 為什么正常打印出的消息會混雜在一起

    image-20250611111700913

這是多線程程序輸出混亂問題,因為多個線程(主線程、新線程 )共享標準輸出流,CPU 調度線程時,沒有加保護時,若一個線程輸出未完成就切換到另一個線程繼續輸出,就會導致消息混雜

2.引入pthread線程庫

為什么會有這個庫,這個庫是什么東西?

image-20250611115557732

我們上面的pthread_create封裝的就是底層系統的clone

image-20250611115740873

我們在c++階段也學習過創建線程的方式,在linux下其本質也是封裝了pthread庫,在windows下是封裝了windows創建線程的接口,目的是為了保證語言的跨平臺、可移植性,所以語言的跨平臺或者可移植性一般都是大力出奇跡,所有平臺全部干一份,然后條件編譯形成庫(所有的熱門偏后端的語言基本多線程都這樣封裝的)

image-20250611163742612

3.linux線程控制的接口

前面我們所提及的pthread庫其實叫做POSIX線程庫

? 與線程有關的函數構成了?個完整的系列,絕?多數函數的名字都是以“pthread_”打頭的

? 要使?這些函數庫,要通過引?頭? <pthread.h>

? 鏈接這些線程函數庫時要使?編譯器命令的“-lpthread”選項

  1. 創建線程函數pthread_create(我們在上面也談過的)

    image-20250611192543920

image-20250611194420935

[^] ?pthread_t類型其實是一個無符號長整型?

在我們的新線程被創建出來之后,主線程和新線程誰先運行是不確定的,這一點在我們的fork創建子進程之后的父子進程之間的誰先運行的時候也是一樣的

這里其實我們的參數3屬于是回調函數的范疇了,說到回調函數,這里就不得不講一下了

關于回調函數:

1. 回調函數的角色

pthread_create 是創建線程的系統調用,需要一個 “線程執行邏輯”,但它沒辦法直接把邏輯寫死在函數里(要支持不同業務場景)。所以設計成讓調用者傳入一個函數指針,這個被傳入的函數(routine)就是 “回調函數”—— 由 pthread_create “回調” 執行,實現線程的自定義邏輯。

2. 代碼里的關鍵關聯

pthread_create(&tid, nullptr, routine, (void *)"thread-1");

routine 的函數簽名要求:必須符合pthread約定的線程函數原型void* ()(void)

即:

  • 返回值是 void*(可用來給主線程返回數據)

  • 參數是 void*(能兼容任意類型的入參,比如這里傳字符串 "thread-1"

調用時機pthread_create 成功創建線程后,新線程會自動執行 routine 函數,把 (void *)"thread-1" 作為參數傳入。

解耦思想pthread_create 只負責 “線程創建 + 觸發回調”,具體線程要做什么(routine 里的邏輯)交給調用者實現,靈活適配不同需求。

總結:回調函數是一種 “反向調用” 設計,pthread_create 預先留好 “函數指針的坑”,你填自己的 routine 邏輯,讓線程按你的邏輯跑。核心是解耦框架(pthread)和業務邏輯(你要線程做的事),讓代碼更靈活。

線程創建好之后,新線程要被主線程等待,不然就會產生類似僵尸進程的問題,導致內存泄漏

  1. 我們可以通過pthread_join函數來讓主線程進行等待

    image-20250611193018939

[^] ?參數1是傳新線程id,參數2則是獲取上面pthread_create參數3的返回值(不關心新線程執行的怎么樣,也就是不關心新線程執行的退出結果可以傳nullptr,需要獲取則要取地址傳入一個指針變量才能拿到返回值為void*的變量)?

新線程return

image-20250611210615413

主線程接收

image-20250611210711690

打印的結果就是123

image-20250611211136615

我們可能會有一個疑惑,那就是在進程等待時會有異常相關的字段,為啥線程這里的join卻沒有呢?答:那是因為等待的目標線程如果異常了,整個進程都退出了,包括主線程,所以join異常是沒有意義的,壓根就看不到;join都是基于線程健康跑完的情況,不需要處理異常信號,異常信號是進程要處理的話題!!

如果我們獲取一下新線程的tid,會發現它壓根就不是我們查看的線程lwp

image-20250611195047434

image-20250611195120710

因為lwp是輕量級進程的概念,而我們在用戶上不要看到這個,因為封裝要封裝徹底(不然用戶本來只需要專注于線程就好了,這樣一搞豈不是還需要去了解輕量級進程嘛),我們這里獲取到的線程id就不會是lwp!!

  1. 我們使用pthread_self函數來獲取調用了這個函數的線程的id

    image-20250611200033981

我們通過這個函數來看看pthread_create返回的線程id是否與新線程通過pthread_self獲取到的自己的id相等,進而驗證pthread_create返回的線程id就是新線程的id

#include <iostream>
#include <pthread.h>
#include <thread>
#include <unistd.h>
using namespace std;
?
void FormatId(pthread_t tid)
{printf("新線程通過pthread_self獲取到的自己的id為: %ld\n", tid);
}
?
void *routine(void *args)
{string name = static_cast<const char *>(args);// 獲取該線程的id來驗證一下在主線程中pthread_create中取得的id是否一致pthread_t tid = pthread_self();FormatId(tid);int cnt = 5;while (cnt){cout << "我是一個新線程, 我的名字是: " << name << endl;cnt--;sleep(1);}return nullptr;
}
?
void showid(pthread_t id)
{printf("pthread_create返回的線程id為: %ld\n", id);
}
?
int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, routine, (void *)"thread-1");showid(tid);(void)n;
?// 主線程進行等待pthread_join(tid, nullptr);
?return 0;
}

image-20250611200748779

通過結果可以看到pthread_create返回的線程id就是新線程的id!!

關于上述代碼的一些結論:

  1. main函數結束,代表主線程結束,一般也代表著進程結束

  2. 新線程對應的入口函數運行結束,代表當前線程運行結束

  3. 給線程傳遞參數和返回值,可以是任意類型,不一定非得是內置類型,我們自定義類型對象也可以

線程終止問題:

  1. 線程的入口函數進行return就是線程終止(這種方式用的最多

  2. 注意:線程不能用exit()終止,因為exit是終止進程的

  3. 線程要終止也可用pthread_exit()函數,可以終止調用這個函數的線程

    image-20250612143839801

    [^]: return res等價于pthread_exit(res)

  4. 終止線程還可以使用pthread_cancel函數,一般都是由主線程來用這個函數來取消新線程,用此方式終止線程的退出結果是-1【PTHREAD_CANCELED】

    image-20250612144641775

問題:如果主線程不想再關心新線程,而是當新線程結束的時候,讓它自己進行釋放,此時我們要如何做呢?

解決方案:設置新線程為分離狀態

技術層面:線程默認是需要被等待的,狀態是joinable;如果不想讓主線程等待新線程,

想讓新線程結束之后,自己退出,設置為分離狀態(!joinable or detach)

理解層面:線程分離,可以是主線程分離新線程,也可以是新線程把自己分離

注意:分離的線程依舊在進程的地址空間中,進程的所有資源,被分離的線程依舊可以訪問,可以操作,只不過主線程不等待新線程了

分離操作:

  1. 可以使用pthread_detach函數進行分離

image-20250612191831630

[^] ?新線程分離自己可用pthread_detach(pthread_self())?

如果線程被設置為分離狀態,不需要進行join等待,join會失敗

實現一個簡單的多線程代碼:

#include <iostream>
#include <vector>
#include <pthread.h>
#include <thread>
#include <string.h>
#include <unistd.h>
using namespace std;
?
// 創建多線程
?
const int num = 10;
?
void *routine(void *args)
{string name = static_cast<const char *>(args);delete args;int cnt = 5;while (cnt--){cout << "new線程名字: " << name << endl;sleep(1);}
?return nullptr;
}
?
int main()
{vector<pthread_t> tids;for (int i = 0; i < num; i++){pthread_t tid;// 我們采用下面這種id的做法是有問題不安全的// 因為傳的指向id的首地址,各線程看到的是同一個id緩沖區,那么最后線程打出來的id// 都會是最后一次循環時修改覆蓋掉前面內容的thread-9// char id[64];// 需要的是,每一次循環,都給對應的線程申請堆空間,這樣才能讓這一循環中創建的新線程// 獨享這塊堆空間的起始地址char *id = new char[64];snprintf(id, 64, "thread-%d", i); // 將后面的格式化輸出到id緩沖區中int n = pthread_create(&tid, nullptr, routine, id);if (n == 0){tids.push_back(tid);}else{continue;}}
?// 主線程才往下走for (int i = 0; i < num; i++){// 一個一個的等待int n = pthread_join(tids[i], nullptr);if (n == 0){cout << "等待新線程成功" << endl;}}
?return 0;
}

我們可以自主封裝一個線程接口類,具體可見:thread/Thread.hpp?

3.線程id及進程地址空間布局

image-20250612202848813

線程的概念是在庫中維護的(linux所有的線程都在庫中),在庫內部就一定會存在多個被創建好的線程,庫當然要管理這樣線程,管理的方法也還是先描述,再組織

會有struct tcb這樣的結構體,當我們調用pthread_create時,這個pthread_create內部就會幫我們在系統當中申請對應的tcb,就如同我們的fopen調用時會在內部申請FILE對象

struct tcb
{//線程應該有的屬性,用戶需要的線程狀態線程id線程獨立的棧結構線程棧大小...(而像優先級、時間片、上下文這種用戶不需要的與調度有關的屬性是被寫到內核的lwp->pcb中)
}

image-20250613133353493

image-20250613133215707

[^] ?我們上面的tid其實就是線程在庫中對應管理塊(紅框)的起始虛擬地址,當線程return退出后,管理塊中的數據并沒有被釋放,所以得join,傳入起始地址用來釋放以及通過ret取到管理塊中保存的結果?

image-20250613144504309

image-20250613144157199

通過上圖中每個管理塊都有線程棧,我們可以知道,每個線程都必須有自己獨立的棧空間(在申請的管理塊當中),主線程則用的是地址空間中的棧,新線程用的是自己申請的管理塊中的棧,所以說每個線程都必須有自己獨立的棧結構

image-20250613135159367

庫中創建好管理塊把一些數據給線程,直接等該線程執行對應方法就好了

linux 用戶級線程 :內核lwp = 1:1

在前面加上__thread的變量會分別在不同線程的線程局部存儲位置開辟一份,名字一樣,但是底層的虛擬地址不一樣了,就可以實現全局變量變成分別新線程的局部變量了

image-20250615161641476

線程的局部存儲有什么用:有時我們需要全局變量,但又不想讓這個全局變量被其他線程看到時就可以在這個變量前面加上__thread

(但是線程局部存儲只能存儲內置類型和部分指針)

4.線程棧

獨立的上下文:有獨立的PCB(內核)+TCP(用戶層,pthread庫內部)

獨立的棧:每個線程都有自己的棧,要么是線程自己的,要么是庫中創建進程時mmap申請出來的

雖然 Linux 將線程和進程不加區分的統?到了 task_struct ,但是對待其地址空間的stack還是有些區別的。

? 對于 Linux 進程或者說主線程,簡單理解就是main函數的棧空間,在fork的時候,實際上就是復制了?親的stack 空間地址,然后寫時拷?(cow)以及動態增?。如果擴充超出該上限則棧溢出會報段錯誤(發送段錯誤信號給該進程)。進程棧是唯?可以訪問未映射??不?定會發?段錯誤?超出擴充上限才報。

? 然?對于主線程?成的?線程??,其 stack將不再是向下??的,?是事先固定下來的。線程棧?般是調?glibc/uclibc等的pthread 庫接? pthread_create創建的線程,在?件映射區(或稱之為共享區)。其中使?mmap 系統調?,這個可以從 glibc的nptl/allocatestack.c 中的 allocate_stack函數中看到:

mem = mmap (NULL, size, prot,MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);

此調?中的 size 參數的獲取很是復雜,你可以??傳?stack的??,也可以使?默認的,?般??就是默認的 8M 。這些都不重要,重要的是,這種stack不能動態增?,?旦?盡就沒了,這是和?成進程的fork不同的地?。在glibc中通過mmap得到了stack之后,底層將調? sys_clone系統調?:

image-20250613193005380

因此,對于?線程的 stack ,它其實是在進程的地址空間中map出來的?塊內存區域,原則上是線程私有的,但是同?個進程的所有線程?成的時候,是會淺拷??成者的 task_struct的很多字段,如果愿意,其它線程也還是可以訪問到的,于是?定要注意

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

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

相關文章

力扣面試150題--只出現一次的數字

Day 91 題目描述## 思路 交換律&#xff1a;a ^ b ^ c <> a ^ c ^ b 任何數于0異或為任何數 0 ^ n > n 相同的數異或為0: n ^ n > 0 根據以上 很容易想到做法&#xff0c;將數組中所有的數異或起來&#xff0c;得到的就是只出現一次的數 class Solution {public in…

【運維基礎】Linux 進程調度管理

Linux 進程調度管理 進程調度器 現代計算機系統中既包含只有單個CPU且任何時候都只能處理單個指令的低端系統到具有幾百個cpu、每個cpu有多個核心的高性能超級計算機&#xff0c;可以并行執行幾百個指令。所有這些系統都有一個共同點&#xff1a;系統進程線程數量超出了CPU數量…

深度學習篇---層與層之間搭配

在深度學習中&#xff0c;各種層&#xff08;比如卷積層、激活函數、池化層等&#xff09;的搭配不是隨意的&#xff0c;而是像 “搭積木” 一樣有規律 —— 每一層的作用互補&#xff0c;組合起來能高效提取特征、穩定訓練&#xff0c;最終提升模型性能。下面用通俗易懂的方式…

服務器多線主要是指什么?

在數字化的網絡環境當中&#xff0c;服務器已經成為各個企業提升線上業務發展的重要網絡設備&#xff0c;其中服務器多線則是指一臺服務器中能夠同時接入多個網絡運營商&#xff0c;并且通過智能路由技術實現用戶訪問請求的自動化分配&#xff0c;大大提高了用戶訪問數據信息的…

從0到1學PHP(三):PHP 流程控制:掌控程序的走向

目錄一、條件語句&#xff1a;程序的 “抉擇路口”1.1 if 語句家族&#xff1a;基礎與進階1.2 switch 語句&#xff1a;精準匹配的 “導航儀”二、循環語句&#xff1a;程序的 “重復舞步”2.1 for 循環&#xff1a;有序的 “征程”2.2 while 與 do - while 循環&#xff1a;條…

uni-app框架基礎

闡述 MVC 模式1, MVC與MVVMMVC 他是后端的一個開發思想MVVM是基于MVC中的view這層所分離出來的一種設計模式。MVC架構詳解MVC&#xff08;Model-View-Controller&#xff09;是一種廣泛使用的軟件設計模式&#xff0c;主要用于分離應用程序的業務邏輯、用戶界面和輸入控制。這種…

智慧收銀系統開發進銷存庫存統計,便利店、水果店、建材與家居行業的庫存匯總管理—仙盟創夢IDE

在零售與批發行業的數字化轉型中&#xff0c;當前庫存匯總作為進銷存管理的核心環節&#xff0c;直接影響著企業的資金周轉、銷售決策和客戶滿意度。現代收銀系統已超越傳統的收款功能&#xff0c;成為整合多渠道數據、實現實時庫存匯總的中樞神經。本文將深入剖析便利店、水果…

selenium(WEB自動化工具)

定義解釋 Selenium是一個用于Web應用程序測試的工具。Selenium測試直接運行在瀏覽器中&#xff0c;就像真正的用戶在操作一樣。支持的瀏覽器包括IE&#xff08;7, 8, 9, 10, 11&#xff09;&#xff0c;Mozilla Firefox&#xff0c;Safari&#xff0c;Google Chrome&#xff0…

windows本地使用conda部署Open-webui

前提條件 Open-webui使用python3.11.9 步驟 conda操作也可以參考 安裝python torch、transformer、記錄 1、conda環境 # 創建環境 conda create --name openwebui python3.11.9# 激活環境 conda activate openwebui# 升級pip版本 pip install --upgrade pip# pip安裝openwe…

【Unity筆記04】數據持久化

&#x1f31f; 方案核心思想遵循以下設計原則&#xff1a;數據安全第一&#xff1a;絕不使用明文存儲&#xff0c;采用AES加密算法保護數據。性能優化&#xff1a;使用異步I/O操作&#xff0c;避免阻塞主線程導致游戲卡頓。結構清晰&#xff1a;模塊化設計&#xff0c;職責分離…

深入理解 HTML5 Web Workers:提升網頁性能的關鍵技術解析

深入理解 HTML5 Web Workers&#xff1a;提升網頁性能的關鍵技術解析引言1. 什么是 Web Workers&#xff1f;Web Workers 的特點&#xff1a;2. Web Workers 的使用方式2.1 創建一個 Web Worker步驟 1&#xff1a;創建 Worker 文件步驟 2&#xff1a;在主線程中調用 Worker3. W…

會議室預定系統核心技術:如何用一行SQL解決時間沖突檢測難題

文章目錄 一、為什么時間沖突檢測是預定系統的核心挑戰? 二、黃金法則:兩行線段重疊檢測法 三、四大沖突場景實戰解析(同一會議室) 四、生產環境完整解決方案 1. 基礎沖突檢測函數 2. 預定API處理流程 3. 高級邊界處理技巧 五、性能優化關鍵策略 六、不同數據庫的適配方案 …

13.正則表達式:文本處理的瑞士軍刀

正則表達式&#xff1a;文本處理的瑞士軍刀 &#x1f3af; 前言&#xff1a;當文本遇上神奇的密碼 想象一下&#xff0c;你是一個圖書管理員&#xff0c;面對著一堆亂七八糟的書籍信息&#xff1a; “聯系電話&#xff1a;138-1234-5678”“郵箱地址&#xff1a;zhang.sangm…

linux下c語言訪問mysql數據庫

一、連接數據庫基礎1. 頭文件與庫文件連接 MySQL 需包含的頭文件&#xff1a;#include <mysql/mysql.h> // 部分環境也可用 #include <mysql.h> 編譯鏈接時&#xff0c;Linux 平臺需指定庫名&#xff1a;-lmysqlclient &#xff0c;用于鏈接 MySQL 客戶端函數庫。2…

6. 傳輸層協議 UDP

傳輸層負責數據能夠從發送端傳輸接收端.1. 再談端口號端口號(Port)標識了一個主機上進行通信的不同的應用程序在 TCP/IP 協議中, 用 "源 IP", "源端口號", "目的 IP", "目的端口號", "協議號" 這樣一個五元組來標識一個通信…

vue 開發總結:從安裝到第一個交互頁面-與數據庫API

vue 總結 1、安裝vue&#xff1a; WinR 輸入&#xff1a;cnpm install -g vue/cli 驗證是否安裝成功&#xff1a;vue --version 2、新建Vue工程 在對應文件夾下右擊打開集成終端 輸入 vue create query_system&#xff08;新建項目名字&#xff09;名稱不能存在大寫&#x…

運維筆記:HTTP 性能優化

一、HTTP 協議特性與性能瓶頸1.1 HTTP 協議發展歷程HTTP 協議的演進直接影響著 Web 性能&#xff0c;各版本關鍵特性對比&#xff1a;協議版本發布時間核心特性性能優勢局限性HTTP/1.01996 年無狀態、短連接簡單易實現每次請求需建立 TCP 連接HTTP/1.11999 年長連接、管道化減少…

ubuntu:運行gcfsd-admin守護進程需要認證,解決方法

這里有個鎖子&#xff0c;每次進入都要輸入密碼&#xff0c;怎么解決&#xff1f; 重新掛載 /data 磁盤 sudo umount /data sudo ntfsfix /dev/sda1 sudo mount -o rw /dev/sda1 /data

1.DRF 環境安裝與配置

文章目錄一. Django Rest_Framework二、環境安裝與配置2.1 安裝 DRF2.2 創建Django項目2.3 添加 rest_framework 應用三、啟動項目一. Django Rest_Framework 核心思想&#xff1a;大量縮減編寫 api 接口的代碼 Django REST framework 是一個建立在 Django 基礎之上的 Web 應…

設計模式(十九)行為型:備忘錄模式詳解

設計模式&#xff08;十九&#xff09;行為型&#xff1a;備忘錄模式詳解備忘錄模式&#xff08;Memento Pattern&#xff09;是 GoF 23 種設計模式中的行為型模式之一&#xff0c;其核心價值在于在不破壞封裝性的前提下&#xff0c;捕獲并外部化一個對象的內部狀態&#xff0c…