線程概念與控制(中)https://blog.csdn.net/Small_entreprene/article/details/146539064?sharetype=blogdetail&sharerId=146539064&sharerefer=PC&sharesource=Small_entreprene&sharefrom=mp_from_link對于之前學習的內容,我們現在知道了:
- Linux沒有真正的線程,他是用輕量級進程模擬的,目前我們只停留在見到了(LWP)
- Linux操作系統提供的接口中,沒有直接提供線程的相關接口
- 作為用戶,Linux和用戶還有一道鴻溝,所以需要在用戶層,需要封裝輕量級進程,形成原生線程庫-pthread!(用戶級別的庫)(我們的可執行程序加載,形成進程,動態鏈接和動態地址重定位,并且要將動態庫,加載到內存,然后映射到當前進程的地址空間中!!!)
線程ID及其進程地址空間布局
在 Linux 系統中,用戶程序與內核之間存在一層抽象,用戶程序需要通過用戶級別的庫來與內核交互。為了方便用戶程序創建和管理線程,Linux 提供了 pthread
(POSIX 線程)庫,這是一個輕量級的線程庫,封裝了底層的線程管理機制。當用戶程序加載并運行時,它會通過動態鏈接和動態地址重定位將 pthread
庫加載到內存中,并將其映射到當前進程的地址空間,從而允許程序創建和管理多個線程,實現并發執行。
這張圖描繪了Linux系統中使用pthread
庫創建線程時的內存布局和動態鏈接過程:首先,pthread.so
庫文件從磁盤加載到物理內存中,并通過動態鏈接映射到進程的地址空間,包括代碼區、數據段和堆區;進程利用mmap
系統調用分配動態映射區域或共享區,用于線程間數據共享;每個線程在內核中有一個task_struct
結構體來跟蹤其狀態;線程各自擁有獨立的棧空間;用戶通過pthread_create()
和pthread_join()
等函數在用戶空間中管理線程,這些函數通過系統調用與內核交互,實現線程的創建和同步。
所以,通過pthread_create()
和pthread_join()
等函數,進程自己的代碼和數據就可以訪問到pthread庫內部的代碼或者數據了!!!
線程的概念是在庫中維護的,因為系統不提供,但是用戶需要,所以在庫內部(庫也是軟件),就可能存在多個線程,每個線程有不同的狀態,這就需要被庫管理起來---先描述再組織!!!
在Linux系統中,線程的概念并不是直接由系統內核提供的,而是通過用戶空間的庫來實現的,其中pthread
庫就是封裝了線程管理功能的原生線程庫。這個庫內部維護了線程的屬性和狀態,包括每個線程的不同狀態,這些信息被組織和管理起來以支持多線程操作。具體來說,Linux中沒有真正意義上的線程,而是使用輕量級進程(LWP)模擬實現線程概念。每個線程在pthread
庫中都有一個對應的屬性結構體,即線程控制塊(Thread Control Block,TCB),用于存儲線程的狀態信息、調度信息、棧信息等。這個結構體在Linux下通常被稱為struct pthread
,它包含了線程的所有必要信息,以便庫可以有效地管理線程。?
在Linux系統中,線程控制塊(Thread Control Block,TCB)是用于存儲用戶線程所有信息的數據結構。TCB的體量比進程控制塊(PCB)小非常多,它包含了線程的狀態信息、線程的調度信息、線程的棧信息等。具體來說,TCB中通常包含以下信息:
線程標識符:為每個線程賦予一個唯一的線程標識符。
一組寄存器:包括程序計數器PC、狀態寄存器和通用寄存器的內容。
線程運行狀態:用于描述線程正處于何種運行狀態。
優先級:描述線程執行的優先程度。
線程專有存儲區:用于線程切換時存放現場保護信息,和與該線程相關的統計信息等。
信號屏蔽:即對某些信號加以屏蔽。
堆棧指針:在線程運行時,經常會進行過程調用,而過程的調用通常會出現多重嵌套的情況,這樣,就必須將每次過程調用中所使用的局部變量以及返回地址保存起來。為此,應為每個線程設置一個堆棧,用它來保存局部變量和返回地址。相應地,在TCB中,也須設置兩個指向堆棧的指針:指向用戶自已堆棧的指針和指向核心棧的指針。
Linux的線程TCB(或模擬的TCB)包含了以下關鍵信息:線程ID:唯一標識線程的標識符,確保每個線程都可以被唯一識別。在
pthread
庫中,TCB通常被表示為struct pthread
,它包含了線程的狀態信息、線程的調度信息、線程的棧信息等。
我怎么能過夠在庫里面創建一個TCB呢?如果理解不了,我們可以想象一下,我們之前學習C語言,包括文件系統,文件描述符的時候,我們fopen的時候會給我們返回一個FILE*的對象:
FILE *fp = fopen();
其實fopen內部會為我們malloc對應的FILE對象,然后再將地址返回。我們之前不光將其封裝了,還將其打包成了庫,然后讓別人使用,所以,為什么我們能夠創建這個TCB,根本原因是我們會調用pthread_create(),其內部就會在系統當中申請相應的TCB,如同fopen也是C標準庫,在庫里面為我們申請struct file對象。?
上面TCB的內容沒有寫時間片,上下文...這些與調度有關的是寫在內核當中的LWP,也就是PCB中。所以線程的概念:一部分在內核中實現,一部分在用戶層來實現。
優點難懂,我們再來看一張圖:
上面的內容無非就是闡述說:在我們自己的代碼區里,我們調用了?pthread_create()
和pthread_join()
等函數,會動態的讓我們在動態庫里面,為我們創建一個TCB,TCB描述了線程的相關屬性。
那如果庫中創建了10個TCB,我們應該如何進行組織呢?
我們看圖,其實是我們創建了一個線程之后,那么在庫內部就創建了一個:(標紅區域:一個管理塊:重點由三部分構成:線程TCB,線程局部存儲,線程棧)
當我們創建第二個線程的時候,會在動態庫中,依據上一個緊挨著的申請一段空間(真實情況不一定挨著),我們管理多線程的數據結構視為數組,其中我們之前代碼測試出的pthread_create的返回值,那么大的數字,其實是線程在庫當中的,對應的管理塊的虛擬地址!!! (函數的返回值就是該管理塊的起始地址!!!)
這時候,我們來談談為什么線程需要join:
在struct pthread中有一個成員變量void *ret,當對應線程執行完之后,return (void*)10;的時候,其實就會將返回值寫到當前該線程的struct pthread中的成員變量void *ret里,所以該線程運行結束了,但是運行結束之后,對應得管理塊并沒有被釋放,所以主線程需要join,因為線程結束時,只是它執行函數完了,但是在庫里面,線程控制塊并沒有結束!!!
并且我們join的時候必須傳入對用的tid,找到對應的TCB,拷貝出結構體當中的ret的內容:
join后再將其釋放,解決內存泄漏問題!!! (也是我們為什么要使用二級指針的原因:返回值是拷貝出來的)
還有:
創建一個線程就會有對應的線程棧,所以每一個線程,必須有自己獨立的棧空間,所對應的棧空間是在自己的pthread庫內部,在自己申請的管理塊當中,這個棧也有自己的起始虛擬地址,所以主線程用進程地址空間的棧,而創建出來的新線程是使用自己對應的線程棧,所以沒有每一個線程都要有自己獨立的棧結構;
那么,用戶線程和LWP是如何進行聯動的呢??
用戶代碼調用: pthread_create()
1. 線程控制塊的創建
當調用 pthread_create()
時,線程庫(如 NPTL)會在用戶空間中為新線程分配一個線程控制塊(struct pthread
)。這個結構體包含了線程的各種屬性,例如線程ID、線程棧指針、線程局部存儲等。線程控制塊存儲在進程的共享區中,所有線程都可以訪問這個區域。
2. 內核中的輕量級進程(LWP)的創建
在底層,pthread_create()
會通過系統調用 clone()
來創建一個輕量級進程(LWP)。clone()
是 Linux 提供的一個系統調用,用于創建輕量級進程,它允許進程共享資源。pthread_create()
實際上是 clone()
的一個封裝。
線程的棧空間是線程運行時用于存儲局部變量、函數調用的返回地址等信息的內存區域。在 pthread_create()
中,線程的棧空間通常是在用戶空間中分配的。對于主線程,其棧空間是進程地址空間中原生的棧;而對于其他線程,棧空間是在共享區中分配的。
在調用 clone()
時,需要指定子進程(線程)的棧地址。這個棧地址通常是指向分配的棧空間的頂部。例如:
char *stack = malloc(STACK_SIZE);
pid_t pid = clone(child_func, stack + STACK_SIZE, SIGCHLD, NULL);
這里,stack + STACK_SIZE
指向棧的頂部。
這時候我們的內核數據和用戶數據就在一定層度上聯動起來了!!!(調用pthread_create,既在庫中創建線程控制的管理塊,又在內核中,調用clone來創建輕量級進程!)
用戶線程和LWP的聯動主要體現在線程的創建、調度和銷毀過程中。當調用pthread_create()
時,線程庫在用戶空間創建線程控制塊,并通過clone()
系統調用在內核中創建一個LWP,將用戶線程與LWP關聯起來。線程運行時,線程庫在用戶空間負責線程的切換,而內核通過LWP的調度管理線程的執行。當線程阻塞或就緒時,線程庫會通知內核更新LWP的狀態,內核根據這些狀態調整調度策略。銷毀線程時,線程庫清理用戶空間資源,并通過系統調用通知內核銷毀對應的LWP。這種聯動機制使得線程能夠在用戶空間高效切換,同時利用內核的資源管理功能,確保線程的高效運行和資源的合理分配。(代購:用戶層是派發購買任務的,LWP內核層是去完成這個任務的,這么完成的,用戶層不關心,只需要完成了,將返回結果帶回給struct pthread就可以了)
在Linux操作系統中,Linux用戶及線程 : 內核LWP = 1 : 1的,對于其他OS,可能是1 : n的。
現在我們就可以粗力度的解決下列問題:
- 線程ID:是我們pthread_create的時候,我們在庫當中創建的描述線程的線程控制塊的起始虛擬地址,所以導致地址非常大;
- 線程返回值:是線程執行完,將該線程的退出結果寫到線程控制塊的對應的結構體的void* ret的內容當中,然后通過join得到;
- 線程分離:在線程控制塊(TCB)中,有一個線程狀態,默認int joinable = 1,表明這個線程不分離,0的時候是分離的,線程一旦在底層退出了,識別到上層控制塊對應結構體里面joinable的字段為0,那么該線程就自動釋放。(joinable本質就是一個標志位)
因為動態庫是共享的,可以被映射到對應進程的虛擬地址空間上,所以Linux所有線程,都在庫中:
只不過互相訪問不了,因為每一個線程只能拿到自己線程控制塊的虛擬地址。
這里創建線程要申請的線程控制塊不是通過malloc出來的,是通過mmap機制申請出來的,其實mmap就是共享內存,只不過我們執勤學習的共享內存是System V標準,mmap是POSIX標準的:用于將文件或設備映射到進程的地址空間。它是一種內存映射文件 I/O 的方法,允許進程像操作內存一樣直接訪問文件內容。
也就是mmap是物理地址空間的一段共享內存,可以映射到不同進程的虛擬地址空間上,如果內容在磁盤上,就可以將文件內容映射到物理共享內存,不同進程就可以訪問該共享內存,達到不需要文件描述符,就可以訪問磁盤的文件。所以mmap可以實現線程申請空間,進程間通信,文件映射。
線程棧
通過上面的學習,我們知道了每一個線程都有自己獨立的棧結構了:
每一個線程都有:
獨立的上下文:有獨立的PCB(內核)+TCB(用戶層,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 系統調?:
int sys_clone(struct pt_regs *regs)
{unsigned long clone_flags;unsigned long newsp;int __user *parent_tidptr, *child_tidptr;clone_flags = regs->bx;// 獲取了 mmap 得到的線程的 stack 指針newsp = regs->cx;parent_tidptr = (int __user *)regs->dx;child_tidptr = (int __user *)regs->di;if (!newsp)newsp = regs->sp;return do_fork(clone_flags, newsp, regs, 0, parent_tidptr, child_tidptr);
}
因此,對于?線程的 stack ,它其實是在進程的地址空間中 map 出來的?塊內存區域,原則上是線程私有的(對應線程控制塊才能過找到:其實都能是“共享的”,只是能不能找到對應的虛擬地址),但是同?個進程的所有線程?成的時候,是會淺拷??成者的 task_struct 的很多字段,如果愿意,其它線程也還是可以訪問到的,于是?定要注意。
線程封裝
有了上面的只是理論儲備,接下來,我們來封裝一下線程,以便更好的認識線程:
源代碼第一版
#ifndef _THREAD_H_
#define _THREAD_H_#include <iostream>
#include <string>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <functional>namespace ThreadModlue
{// 用于生成線程名稱的序號// 注意:此變量是靜態的,僅在當前文件內可見。如果此頭文件被多個源文件包含,// 可能會導致每個文件中都有一個獨立的 `number`,從而導致線程名稱重復。static uint32_t number = 1;class Thread{// 使用 std::function 封裝線程的回調函數,支持函數指針、lambda 表達式等using func_t = std::function<void()>;private:// 設置線程為分離狀態void EnableDetach(){std::cout << "線程被分離了" << std::endl;_isdetach = true;}// 設置線程為運行狀態void EnableRunning(){_isrunning = true;}// 線程的入口函數,必須是靜態函數或全局函數,因為 pthread_create 要求入口函數為 C 風格static void *Routine(void *args){// 將傳入的 void* 轉換為 Thread 類型的指針Thread *self = static_cast<Thread *>(args);// 設置線程為運行狀態self->EnableRunning();// 如果線程被分離,則調用 Detach 方法if (self->_isdetach)self->Detach();// 設置線程名稱,注意:pthread_setname_np 是非標準擴展,可能在某些平臺上不可用pthread_setname_np(self->_tid, self->_name.c_str());// 調用用戶提供的回調函數self->_func();// 返回 nullptr 表示線程正常結束return nullptr;}public:// 構造函數,初始化線程對象Thread(func_t func): _tid(0), // 線程 ID 初始化為 0_isdetach(false), // 線程默認不是分離狀態_isrunning(false), // 線程默認未運行res(nullptr), // 線程返回值初始化為 nullptr_func(func) // 用戶提供的回調函數{// 生成線程名稱,格式為 "thread-序號"_name = "thread-" + std::to_string(number++);}// 分離線程void Detach(){// 如果線程已經被分離,則直接返回if (_isdetach)return;// 如果線程正在運行,則調用 pthread_detach 將線程分離if (_isrunning)pthread_detach(_tid);// 設置線程為分離狀態EnableDetach();}// 啟動線程bool Start(){// 如果線程已經在運行,則返回 falseif (_isrunning)return false;// 創建線程,將當前對象的地址傳遞給線程入口函數int n = pthread_create(&_tid, nullptr, Routine, this);// 如果創建失敗,打印錯誤信息并返回 falseif (n != 0){std::cerr << "create thread error: " << strerror(n) << std::endl;return false;}else{// 打印線程創建成功的信息std::cout << _name << " create success" << std::endl;return true;}}// 停止線程bool Stop(){// 如果線程不在運行,則返回 falseif (!_isrunning)return false;// 發送取消請求給線程int n = pthread_cancel(_tid);// 如果取消失敗,打印錯誤信息并返回 falseif (n != 0){std::cerr << "stop thread error: " << strerror(n) << std::endl;return false;}else{// 設置線程為非運行狀態_isrunning = false;// 打印線程停止的信息std::cout << _name << " stop" << std::endl;return true;}}// 等待線程結束void Join(){// 如果線程已經被分離,則不能調用 joinif (_isdetach){std::cout << "你的線程已經是分離的了,不能進行join" << std::endl;return;}// 等待線程結束,并獲取返回值int n = pthread_join(_tid, &res);// 如果 join 失敗,打印錯誤信息if (n != 0){std::cerr << "join thread error: " << strerror(n) << std::endl;}else{// 打印 join 成功的信息std::cout << "join success" << std::endl;}}// 析構函數~Thread(){// 注意:當前析構函數為空,沒有處理線程的銷毀。// 如果線程仍在運行,應該等待線程結束或者分離線程,否則可能導致未定義行為。}private:pthread_t _tid; // 線程 IDstd::string _name; // 線程名稱bool _isdetach; // 是否分離bool _isrunning; // 是否運行void *res; // 線程返回值func_t _func; // 用戶提供的回調函數};
}#endif
源代碼詳細解釋
這段代碼實現了一個簡單的線程類封裝,基于 POSIX 線程庫(pthread
)。它提供了一個方便的接口來創建、啟動、停止、分離和等待線程,并且支持線程名稱的設置和線程回調函數的封裝。以下是對代碼的詳細解釋:
1. 頭文件保護
#ifndef _THREAD_H_
#define _THREAD_H_
這是標準的頭文件保護宏,用于防止頭文件被重復包含。如果 _THREAD_H_
已經被定義,則不會再次包含該頭文件,從而避免重復定義的問題。
2. 包含的頭文件
#include <iostream>
#include <string>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <functional>
-
<iostream>
和<string>
提供了輸入輸出流和字符串操作的功能。 -
<pthread.h>
是 POSIX 線程庫的頭文件,提供了線程創建、管理等函數。 -
<cstdio>
和<cstring>
提供了標準輸入輸出和字符串操作的 C 風格函數。 -
<functional>
提供了std::function
,用于封裝可調用對象(如函數指針、lambda 表達式等)。
3. 命名空間和靜態變量
namespace ThreadModlue
{static uint32_t number = 1;
-
ThreadModlue
是一個命名空間,用于封裝線程相關的類和變量,避免命名沖突。 -
number
是一個靜態變量,用于生成線程名稱的序號。它在當前文件內可見,每次創建線程時遞增,用于生成唯一的線程名稱。
4. 線程類的定義
class Thread
{using func_t = std::function<void()>;
-
Thread
類封裝了線程的創建、管理等功能。 -
func_t
是一個類型別名,表示線程的回調函數類型,使用std::function<void()>
,可以接受函數指針、lambda 表達式等可調用對象。
5. 私有成員函數
void EnableDetach()
{std::cout << "線程被分離了" << std::endl;_isdetach = true;
}void EnableRunning()
{_isrunning = true;
}
-
EnableDetach
:將線程標記為分離狀態,并打印提示信息。 -
EnableRunning
:將線程標記為運行狀態。
6. 靜態入口函數
static void *Routine(void *args)
{Thread *self = static_cast<Thread *>(args);self->EnableRunning();if (self->_isdetach)self->Detach();pthread_setname_np(self->_tid, self->_name.c_str());self->_func();return nullptr;
}
-
Routine
是線程的入口函數,必須是靜態函數或全局函數,因為pthread_create
要求入口函數為 C 風格。 -
它接收一個
void*
參數,將其轉換為Thread
類型的指針。 -
設置線程為運行狀態,如果線程被分離,則調用
Detach
方法。 -
使用
pthread_setname_np
設置線程名稱(注意:這是非標準擴展,可能在某些平臺上不可用)。 -
調用用戶提供的回調函數
_func
。 -
返回
nullptr
表示線程正常結束。
7. 公有成員函數
Thread(func_t func): _tid(0), _isdetach(false), _isrunning(false), res(nullptr), _func(func)
{_name = "thread-" + std::to_string(number++);
}
-
構造函數初始化線程對象,設置線程 ID、分離狀態、運行狀態、返回值和用戶提供的回調函數。
-
生成線程名稱,格式為
"thread-序號"
,number
遞增。
void Detach()
{if (_isdetach)return;if (_isrunning)pthread_detach(_tid);EnableDetach();
}
-
Detach
方法將線程分離。如果線程已經在分離狀態,則直接返回;如果線程正在運行,則調用pthread_detach
將線程分離。
bool Start()
{if (_isrunning)return false;int n = pthread_create(&_tid, nullptr, Routine, this);if (n != 0){std::cerr << "create thread error: " << strerror(n) << std::endl;return false;}else{std::cout << _name << " create success" << std::endl;return true;}
}
-
Start
方法啟動線程。如果線程已經在運行,則返回false
。 -
使用
pthread_create
創建線程,將當前對象的地址傳遞給線程入口函數。 -
如果創建失敗,打印錯誤信息并返回
false
;否則打印線程創建成功的信息并返回true
。
bool Stop()
{if (!_isrunning)return false;int n = pthread_cancel(_tid);if (n != 0){std::cerr << "stop thread error: " << strerror(n) << std::endl;return false;}else{_isrunning = false;std::cout << _name << " stop" << std::endl;return true;}
}
-
Stop
方法停止線程。如果線程不在運行,則返回false
。 -
使用
pthread_cancel
發送取消請求給線程。 -
如果取消失敗,打印錯誤信息并返回
false
;否則設置線程為非運行狀態并返回true
。
void Join()
{if (_isdetach){std::cout << "你的線程已經是分離的了,不能進行join" << std::endl;return;}int n = pthread_join(_tid, &res);if (n != 0){std::cerr << "join thread error: " << strerror(n) << std::endl;}else{std::cout << "join success" << std::endl;}
}
-
Join
方法等待線程結束。如果線程已經被分離,則不能調用join
。 -
使用
pthread_join
等待線程結束,并獲取返回值。 -
如果
join
失敗,打印錯誤信息;否則打印成功信息。
8. 私有成員變量
pthread_t _tid;
std::string _name;
bool _isdetach;
bool _isrunning;
void *res;
func_t _func;
-
_tid
:線程 ID。 -
_name
:線程名稱。 -
_isdetach
:是否分離。 -
_isrunning
:是否運行。 -
res
:線程返回值。 -
_func
:用戶提供的回調函數。
9. 析構函數
~Thread()
{// 注意:當前析構函數為空,沒有處理線程的銷毀。// 如果線程仍在運行,應該等待線程結束或者分離線程,否則可能導致未定義行為。
}
-
析構函數目前為空,沒有處理線程的銷毀。
-
如果線程仍在運行,應該等待線程結束或者分離線程,否則可能導致未定義行為。
這段代碼實現了一個簡單的線程類封裝,提供了線程創建、啟動、停止、分離和等待的功能,并支持線程名稱的設置和線程回調函數的封裝。它基于 POSIX 線程庫,使用了 C++ 的 std::function
來支持多種類型的回調函數。
源代碼第二版-tmplate模板化
在之前的代碼中,我們實現了一個簡單的線程類封裝,支持線程的創建、啟動、停止、分離和等待功能。然而,之前的實現存在一些局限性,例如線程回調函數只能是無參的 std::function<void()>
,這限制了線程任務的靈活性。此外,線程名稱的序號變量 number
是靜態的,可能會導致線程名稱重復的問題。
為了進一步提升線程類的靈活性和功能,我們對代碼進行了改進。改進后的代碼支持帶參數的線程回調函數,并且通過模板化的方式,允許用戶傳遞不同類型的數據給線程任務。此外,我們還修復了線程名稱序號變量的潛在問題,確保線程名稱的唯一性。
以下是改進后的代碼,包含詳細的注釋:
#ifndef _THREAD_H_
#define _THREAD_H_#include <iostream>
#include <string>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <functional>namespace ThreadModlue
{// 使用靜態局部變量來生成線程名稱的序號,確保線程名稱的唯一性// 修復了之前靜態變量可能導致的線程名稱重復問題static uint32_t GetThreadId(){static uint32_t number = 1; // 靜態局部變量,只初始化一次return number++; // 返回當前值并遞增}// 模板類 Thread,支持帶參數的線程回調函數template <typename T>class Thread{// 定義線程回調函數的類型,支持帶參數的函數using func_t = std::function<void(T)>;private:// 設置線程為分離狀態void EnableDetach(){std::cout << "線程被分離了" << std::endl;_isdetach = true;}// 設置線程為運行狀態void EnableRunning(){_isrunning = true;}// 線程的入口函數,必須是靜態函數或全局函數// 修復了之前靜態成員函數的潛在問題static void *Routine(void *args){Thread<T> *self = static_cast<Thread<T> *>(args); // 將 void* 轉換為 Thread 類型的指針self->EnableRunning(); // 設置線程為運行狀態if (self->_isdetach)self->Detach(); // 如果線程被分離,則調用 Detach 方法self->_func(self->_data); // 調用用戶提供的回調函數,并傳遞數據return nullptr; // 返回 nullptr 表示線程正常結束}public:// 構造函數,初始化線程對象Thread(func_t func, T data): _tid(0), // 線程 ID 初始化為 0_isdetach(false), // 線程默認不是分離狀態_isrunning(false), // 線程默認未運行res(nullptr), // 線程返回值初始化為 nullptr_func(func), // 用戶提供的回調函數_data(data) // 用戶傳遞給線程的數據{// 生成線程名稱,格式為 "thread-序號"_name = "thread-" + std::to_string(GetThreadId());}// 分離線程void Detach(){if (_isdetach) // 如果線程已經被分離,則直接返回return;if (_isrunning) // 如果線程正在運行,則調用 pthread_detach 將線程分離pthread_detach(_tid);EnableDetach(); // 設置線程為分離狀態}// 啟動線程bool Start(){if (_isrunning) // 如果線程已經在運行,則返回 falsereturn false;int n = pthread_create(&_tid, nullptr, Routine, this); // 創建線程if (n != 0) // 如果創建失敗,打印錯誤信息并返回 false{std::cerr << "create thread error: " << strerror(n) << std::endl;return false;}else // 打印線程創建成功的信息{std::cout << _name << " create success" << std::endl;return true;}}// 停止線程bool Stop(){if (!_isrunning) // 如果線程不在運行,則返回 falsereturn false;int n = pthread_cancel(_tid); // 發送取消請求給線程if (n != 0) // 如果取消失敗,打印錯誤信息并返回 false{std::cerr << "stop thread error: " << strerror(n) << std::endl;return false;}else // 設置線程為非運行狀態并返回 true{_isrunning = false;std::cout << _name << " stop" << std::endl;return true;}}// 等待線程結束void Join(){if (_isdetach) // 如果線程已經被分離,則不能調用 join{std::cout << "你的線程已經是分離的了,不能進行join" << std::endl;return;}int n = pthread_join(_tid, &res); // 等待線程結束if (n != 0) // 如果 join 失敗,打印錯誤信息{std::cerr << "join thread error: " << strerror(n) << std::endl;}else // 打印 join 成功的信息{std::cout << "join success" << std::endl;}}// 析構函數~Thread(){// 注意:當前析構函數為空,沒有處理線程的銷毀。// 如果線程仍在運行,應該等待線程結束或者分離線程,否則可能導致未定義行為。}private:pthread_t _tid; // 線程 IDstd::string _name; // 線程名稱bool _isdetach; // 是否分離bool _isrunning; // 是否運行void *res; // 線程返回值func_t _func; // 用戶提供的回調函數T _data; // 用戶傳遞給線程的數據};
}#endif
改進點說明
線程名稱序號的改進:使用靜態局部變量 GetThreadId
函數來生成線程名稱的序號,確保線程名稱的唯一性。靜態局部變量只在第一次調用時初始化,并且在程序運行期間保持其值,從而避免了之前靜態變量可能導致的線程名稱重復問題。
支持帶參數的線程回調函數:通過模板化的方式,允許用戶傳遞不同類型的數據給線程任務。線程回調函數的類型為 std::function<void(T)>
,其中 T
是用戶定義的數據類型。這樣可以更靈活地處理線程任務。
靜態成員函數的修復:靜態成員函數 Routine
修復了之前可能存在的問題,確保線程入口函數的正確性。
這些改進使得線程類更加靈活和健壯,能夠更好地滿足實際開發中的需求。
線程局部存儲
每一個線程創建時,他會庫里面創建描述線程的結構體struct pthread,內部有指針指向自己對應的線程棧,可是線程局部存儲是個什么東西?
我們先來看一個代碼:
#include <pthread.h>
#include <iostream>
#include <string>
#include <unistd.h>int count = 1;std::string Addr(int &c)
{char addr[64];snprintf(addr, sizeof(addr), "%p", &c);return addr;
}void *routine1(void *args)
{(void)args;while (true){std::cout << "thread - 1, count = " << count << "[我來修改count], "<< "&count: " << Addr(count) << std::endl;count++;sleep(1);}
}void *routine2(void *args)
{(void)args;while (true){std::cout << "thread - 2, count = " << count<< ", &count: " << Addr(count) << std::endl;sleep(1);}
}int main()
{pthread_t tid1, tid2; // 創建兩個線程,分別執行不同的任務pthread_create(&tid1, nullptr, routine1, nullptr);pthread_create(&tid2, nullptr, routine2, nullptr);pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);return 0;
}
代碼中定義了一個全局變量 count
,并創建了兩個線程。第一個線程(routine1
)不斷修改全局變量 count
的值,并打印當前的 count
值和它的地址。第二個線程(routine2
)則只讀取 count
的值并打印。(由于兩個線程同時訪問和修改同一個全局變量,而沒有采取任何同步措施,因此可能會出現競爭條件,導致輸出結果不可預測,甚至可能出現數據錯誤。所以我們通過sleep來確保看到的現象是OK的,主要是看一個現象,對其保護會在后面談到)
一個修改一個打印,因為這是共享的資源,所以不會發生寫時拷貝。
但是我們給全局共享的變量count前加修飾__thread:
我們發現count前加修飾__thread之后,打印出來的結果說明的是:對應兩個的count不再是一個相同的一個地址上的變量了。
所以:我們就稱為:變量count前加修飾__thread后,該count叫做線程的局部存儲!
其實__thread是一個我們引導編譯器的選項,實際上就是我們編譯這一份代碼的時候,這個修飾后的額count并不會在已初始化數據段上去幫我們定義,他會將其count變量在當前線程的局部存儲當中開辟一份,只不過變量名都是count,但是底層的虛擬地址此時就不一樣了。
這個現象,就是線程的局部存儲!
那么線程局部存儲有什么用?
有時候,我們創建線程的時候,我們往往需要有全局變量,比如說。。,但是我又不想讓這個全局變量被其他線程看到!所以我們可是利用__thread來實現線程局部存儲。
官方點就是:
線程局部存儲(Thread Local Storage,TLS)是一種特殊的存儲機制,用于為每個線程提供獨立的變量副本。即使多個線程訪問同一個變量名,它們看到的其實是各自線程中的獨立副本,而不是共享的全局變量。線程局部存儲的主要用途是解決多線程環境中的數據隔離問題,避免線程之間的數據沖突和競爭條件。
1.?避免競爭條件
在多線程程序中,全局變量通常會被多個線程共享和訪問。如果沒有適當的同步機制(如互斥鎖),可能會導致競爭條件,使得程序的行為不可預測。線程局部存儲可以為每個線程提供獨立的變量副本,從而避免這種問題。
2.?線程特定數據
有些數據是線程特定的,每個線程需要有自己的獨立副本。例如:
-
每個線程有自己的日志記錄器、配置信息或用戶上下文。
-
每個線程有自己的臨時存儲空間或緩沖區。
使用線程局部存儲可以方便地管理這些線程特定的數據,而無需手動為每個線程分配和管理獨立的變量。(方便,爽!!!)
3.?性能優化
使用互斥鎖等同步機制雖然可以解決競爭條件,但會引入額外的性能開銷,尤其是在高并發場景下。線程局部存儲可以避免這種開銷,因為每個線程訪問的是自己的獨立副本,無需同步。
我們需要注意的是:
線程局部存儲,只能存儲內置類型和部分指針(不要說class/lambda/函數,這可不行)
pthread_setname_np
和pthread_getname_np
是兩個用于設置和獲取線程名稱的函數,它們在 Linux 系統中廣泛用于調試和監控多線程程序。#include <pthread.h>int pthread_setname_np(pthread_t thread, const char *name); int pthread_getname_np(pthread_t thread, char *name, size_t len);
pthread_setname_np
:
用于為指定線程設置名稱。線程名稱是一個以空字符結尾的字符串,最大長度為 16 個字符(包括空字符)。如果名稱長度超過 16 個字符,函數會返回錯誤碼
ERANGE
。參數:
thread
:要設置名稱的線程的標識符。
name
:指向線程名稱的字符串指針。返回值:
成功時返回 0,失敗時返回錯誤碼。
pthread_getname_np
:
用于獲取指定線程的名稱。名稱存儲在提供的緩沖區中,緩沖區長度至少應為 16 個字符。
參數:
thread
:要獲取名稱的線程的標識符。
name
:用于存儲線程名稱的緩沖區。
len
:緩沖區的長度。返回值:
成功時返回 0,失敗時返回錯誤碼。
?
其原理就是我們的線程局部存儲:
pthread_setname_np
和 pthread_getname_np
是用于設置和獲取線程名稱的函數,它們通過操作線程的內部控制結構(如線程控制塊 TCB)來實現。線程局部存儲(TLS)則是為每個線程提供獨立變量副本的機制,確保線程間數據隔離。雖然設置線程名稱的操作本身不直接涉及 TLS,但它們都基于線程的內部數據結構來管理線程相關的信息,線程名稱和線程局部變量都存儲在每個線程的獨立空間中,從而實現線程級別的數據隔離和管理。(就是可以看成mame就是一個__pthread修飾的全局變量)?