線程概念與控制(下)

線程概念與控制(中)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中通常包含以下信息:

  1. 線程標識符:為每個線程賦予一個唯一的線程標識符。

  2. 一組寄存器:包括程序計數器PC、狀態寄存器和通用寄存器的內容。

  3. 線程運行狀態:用于描述線程正處于何種運行狀態。

  4. 優先級:描述線程執行的優先程度。

  5. 線程專有存儲區:用于線程切換時存放現場保護信息,和與該線程相關的統計信息等。

  6. 信號屏蔽:即對某些信號加以屏蔽。

  7. 堆棧指針:在線程運行時,經常會進行過程調用,而過程的調用通常會出現多重嵌套的情況,這樣,就必須將每次過程調用中所使用的局部變量以及返回地址保存起來。為此,應為每個線程設置一個堆棧,用它來保存局部變量和返回地址。相應地,在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的。

現在我們就可以粗力度的解決下列問題:

  1. 線程ID:是我們pthread_create的時候,我們在庫當中創建的描述線程的線程控制塊的起始虛擬地址,所以導致地址非常大;
  2. 線程返回值:是線程執行完,將該線程的退出結果寫到線程控制塊的對應的結構體的void* ret的內容當中,然后通過join得到;
  3. 線程分離:在線程控制塊(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_nppthread_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_nppthread_getname_np 是用于設置和獲取線程名稱的函數,它們通過操作線程的內部控制結構(如線程控制塊 TCB)來實現。線程局部存儲(TLS)則是為每個線程提供獨立變量副本的機制,確保線程間數據隔離。雖然設置線程名稱的操作本身不直接涉及 TLS,但它們都基于線程的內部數據結構來管理線程相關的信息,線程名稱和線程局部變量都存儲在每個線程的獨立空間中,從而實現線程級別的數據隔離和管理。(就是可以看成mame就是一個__pthread修飾的全局變量)?

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

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

相關文章

SQL注入之盲注技術詳解

SQL注入之盲注技術詳解 一、盲注基本概念盲注特點&#xff1a; 二、盲注主要類型1. 布爾盲注判斷依據&#xff1a; 2. 時間盲注判斷依據&#xff1a; 三、布爾盲注詳細技術1. 識別布爾盲注2. 數據提取技術(1) 判斷數據庫類型(2) 獲取數據庫名長度(3) 逐字符獲取數據庫名(4) 獲取…

OpenCV 圖形API(3)高層次設計概覽

操作系統&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 編程語言&#xff1a;C11 描述 G-API 是一個異構框架&#xff0c;提供了統一的 API 來使用多個支持的后端編程圖像處理流水線。 關鍵的設計理念是在指定使用哪些內核和設備時保持流…

阿里云Tair KVCache:打造以緩存為中心的大模型Token超級工廠

一、Tair KVCache 簡介 Tair KVCache 是阿里云瑤池旗下云數據庫 Tair 面向大語言模型推理場景推出的 KVCache 緩存加速服務。 隨著互聯網技術的演進與流量規模的激增&#xff0c;緩存技術逐漸成為系統架構的核心組件。該階段催生了 Redis 等開源緩存數據庫&#xff0c;阿里巴巴…

Open GL ES ->GLSurfaceView正交投影與透視投影方法中近遠平面取值參考

坐標系 OpenGL ES使用右手坐標系&#xff0c;相機默認朝向負z方向 相機位置|vz軸<----- 0 -----> -near -----> -far -----不可見 可見區域 不可見裁剪規則 只有z值在[-near, -far]范圍內的物體可見&#xff0c; 當z > -near&#xff08;在近平面前&#…

iOS自定義collection view的page size(width/height)分頁效果

前言 想必大家工作中或多或少會遇到下圖樣式的UI需求吧 像這種cell長度不固定&#xff0c;并且還能實現的分頁效果UI還是很常見的 實現 我們這里實現主要采用collection view&#xff0c;實現的方式是自定義一個UICollectionViewFlowLayout的子類&#xff0c;在這個類里對…

Java高頻面試之并發編程-01

hello啊&#xff0c;各位觀眾姥爺們&#xff01;&#xff01;&#xff01;本baby今天來報道了&#xff01;哈哈哈哈哈嗝&#x1f436; 面試官&#xff1a;并行跟并發有什么區別&#xff1f; 并發 vs 并行&#xff1a;核心區別與場景 1. 定義對比 維度并發&#xff08;Concu…

從零開始學Rust:所有權(Ownership)機制精要

文章目錄 第四章&#xff1a;Ownership 所有權核心概念關鍵機制引用與借用&#xff08;Reference & Borrowing&#xff09;懸垂引用問題錯誤示例分析解決方案引用安全規則 切片&#xff08;Slice&#xff09;內存安全保證 第四章&#xff1a;Ownership 所有權 Ownership i…

一旦懂得,有趣得緊1:詞根tempt-(嘗試)的兩種解法

詞根tempt-嘗試 tempt vt.引誘&#xff1b;誘惑&#xff1b;慫恿&#xff1b;利誘&#xff1b;勸誘&#xff1b;鼓動 temptation n.引誘&#xff1b;誘惑 // tempt v.引誘 -ation 名詞后綴 attempt v.&n.嘗試&#xff0c;試圖 // at- 加強 tempt 嘗試contempt n.蔑視&am…

召喚數學精靈

1.召喚數學精靈 - 藍橋云課 問題描述 數學家們發現了兩種用于召喚強大的數學精靈的儀式&#xff0c;這兩種儀式分別被稱為累加法儀式 A(n) 和累乘法儀式 B(n)。 累加法儀式 A(n) 是將從1到 n 的所有數字進行累加求和&#xff0c;即&#xff1a; A(n)12?n 累乘法儀式 B(n) …

C語言實現查表8位SAE J1850 CRC

背景&#xff1a; 在做霍爾采集電流的時候&#xff0c;CSSV1500N 系列電流傳感器通過can數據輸出的報文需要做crc校驗&#xff0c;嵌入式常用查表的方式&#xff0c;所以就問了下deepseek怎么算這個CRC. 以下是使用 查表法&#xff08;Lookup Table&#xff09; 在C語言中高效…

【UE5.3.2】初學1:適合初學者的入門路線圖和建議

3D人物的動作制作 大神分析:3D人物的動作制作通常可以分為以下幾個步驟: 角色綁定(Rigging):將3D人物模型綁定到一個骨骼結構上,使得模型能夠進行動畫控制。 動畫制作(Animation):通過控制骨骼結構,制作出人物的各種動作,例如走路、跳躍、打斗等。 動畫編輯(Ani…

mapreduce的工作原理

MapReduce 是 Hadoop 中實現分布式并行計算的核心框架&#xff0c;其工作原理基于“分而治之”的思想&#xff0c;將大規模數據處理任務分解為 Map&#xff08;映射&#xff09; 和 Reduce&#xff08;歸約&#xff09; 兩個階段。 一、MapReduce 核心流程 1. Input 階段 - 輸…

換季推廣不好做?DeepBI用一鍵托管的方式,讓廣告投放跑得快、準、穩

每年換季&#xff0c;尤其是春夏、秋冬交替的節點&#xff0c;都是電商平臺上各類季節性商品扎堆上新的高峰期。無論是服飾鞋包、家居戶外&#xff0c;還是母嬰用品、美妝護膚&#xff0c;許多商品都有著強烈的“時間窗口效應”——一旦錯過了熱賣期&#xff0c;流量下滑迅速&a…

Qt5.14.2+Cmake使用mingw64位編譯opencv4.5成功圖文教程

? 一、下載安裝相關編譯環境軟件 1.1 Python3.8&#xff1a;安裝路徑:C:\Users\Administrator\AppData\Local\Programs\Python\Python38-32 安裝包&#xff1a;python3.8.exe 1.2 QT5.14.2&#xff1a;安裝路徑:C:\Qt\Qt5.14.2 1.3 opencv4.5&#xff1a;解壓路徑D:\o…

OpenBMC:BmcWeb 處理http請求3 字典樹查找節點

OpenBMC:BmcWeb 處理http請求2 查找路由對象-CSDN博客 findRouteByPerMethod實際上是調用了perMethod.trie.find(url);來查找路由對象的 class Trie {struct FindResult{unsigned ruleIndex;std::vector<std::string> params;};FindResult findHelper(const std::string…

Openssl自簽證書相關知識

1.前提 檢查是否已安裝 openssl $ which openssl /usr/bin/openssl 2.建立CA授權中心 2.1.生成ca私鑰(ca-prikey.pem) 初始化 OpenSSL 證書頒發機構(CA)的序列號文件 在生成證書時,ca.srl 的初始序列號需正確初始化(如 01),否則可能導致證書沖突 這會將 01 顯示在屏幕…

K個一組翻轉鏈表--囊括半數鏈表題的思想

K 個一組翻轉鏈表 這道算法題就是鏈表多個算法思想的結合&#xff0c;解決這一道leetcodehot100的鏈表題至少能做一半了 大概有一下幾個點 1.鏈表定位 2.鏈表翻轉 3.哨兵節點 4.鏈表合并 看看題目 給你鏈表的頭節點 head &#xff0c;每 k 個節點一組進行翻轉&#xff…

Flutter敏感詞過濾實戰:基于AC自動機的高效解決方案

Flutter敏感詞過濾實戰&#xff1a;基于AC自動機的高效解決方案 在社交、直播、論壇等UGC場景中&#xff0c;敏感詞過濾是保障平臺安全的關鍵防線。本文將深入解析基于AC自動機的Flutter敏感詞過濾實現方案&#xff0c;通過原理剖析實戰代碼性能對比&#xff0c;帶你打造毫秒級…

UML中的用例圖和類圖

在UML&#xff08;統一建模語言&#xff09;中&#xff0c;**用例圖&#xff08;Use Case Diagram&#xff09;和類圖&#xff08;Class Diagram&#xff09;**是兩種最常用的圖表類型&#xff0c;分別用于描述系統的高層功能和靜態結構。以下是它們的核心概念、用途及區別&…

深入解析:HarmonyOS Design設計語言的核心理念

深入解析&#xff1a;HarmonyOS Design設計語言的核心理念 在當今數字化迅速發展的時代&#xff0c;用戶對操作系統的體驗要求越來越高。華為的HarmonyOS&#xff08;鴻蒙操作系統&#xff09;應運而生&#xff0c;旨在為用戶提供全場景、全設備的智慧體驗。其背后的設計語言—…