Linux操作系統之線程(五):線程封裝

目錄

前言

一、線程ID及進程地址空間布局

二、線程棧與線程局部存儲

三、線程封裝

總結:


前言

我們在上篇文章著重給大家說了一下線程的控制的有關知識。

但是如果我們要使用線程,就得那這pthread_create接口直接用嗎?這樣豈不是太過麻煩,要知道,C++,java等語言其實都對這個線程進行了封裝,形成了獨屬于自己語言風格的線程。

今天,我們不僅要來給大家補充一些知識,還會給大家模擬實現一下一個簡單的線程封裝,希望能夠幫助大家更好的學習線程。

一、線程ID及進程地址空間布局

我們之前使用pthread_create的時候曾經提到了線程ID。

我們知道,Linux中沒有真正意義上的線程,只有輕量級進程。

每一個線程的數據結構其實都是PCB,所以針對每一個PCB,每一個線程(輕量級進程時調度的最小單位),都會有一個對應的ID來表示該線程,這個ID跟我們學習進程時的pid_t差不多。

但是實際上,pthread_create函數在使用時會產生一個線程ID,并將其存放在第一個參數指向的內存位置。pthread_create返回的線程ID實際上是NPTL線程庫在用戶空間分配的一個內存地址,這個地址指向線程控制塊(TCB),作為線程庫內部管理線程的標識符。線程庫的后續操作,都是根據這個線程ID來操作的。

線程庫提供了pthread_self函數,可以獲得線程自身的ID:

pthread_t到底是什么類型呢?這取決與實現。
對于Linux目前實現的NPTL實現而言,pthread_t類型的現場ID,本質上就是一個進程地址空間的地址。
#define _GNU_SOURCE
#include <stdio.h>
#include<iostream>
#include <pthread.h>
#include <unistd.h>void* thread_func(void* arg) 
{// 獲取當前線程的 pthread_tpthread_t self_id = pthread_self();// 打印 pthread_t 的值(以指針和整數形式)printf("Thread ID (pthread_self):\n");printf("  As pointer: %p\n", (void*)self_id);printf("  As unsigned long: %lu\n", (unsigned long)self_id);return NULL;
}int main() 
{pthread_t tid;// 創建線程pthread_create(&tid, NULL, thread_func, NULL);// 打印主線程看到的 pthread_tstd::cout<<tid<<std::endl;printf("Main thread sees new thread ID:\n");printf("  As pointer: %p\n", (void*)tid);printf("  As unsigned long: %lu\n", (unsigned long)tid);pthread_join(tid, NULL);return 0;
}

可以看出,實質上就是一個地址


我們之前學過一點庫。可以知道,我們是先將pthread庫加載到物理內存中,通過映射,讓自己被看見(共享):

所以庫也是共享的,那如果有一百個線程,庫的內部豈不是要維護一百份線程的屬性集合?庫要不要對線程屬性進行管理?

:要,怎么管理?:先描述,再組織。

所以有一個結構體,叫做TCB。?這就跟你去圖書館查閱資料一樣,圖書館的書都是共享的,但是你需要讀者借閱卡。

可以這樣理解Linux線程的管理機制:主線程的進程控制塊(PCB)通過mmap區域維護著與線程庫(libpthread.so)的映射關系,而線程庫內部使用一個稱為TCB(線程控制塊)的關鍵數據結構來管理線程資源。

每個TCB不僅保存著對應線程的pthread_t標識符(實際上就是TCB自身的地址指針),還記錄了該線程獨立分配的棧空間信息,包括棧的起始虛擬地址和結束虛擬地址。這些TCB通過鏈表等形式組織起來,使得線程庫能夠高效地管理所有線程的私有數據和執行上下文,而內核則只需關注輕量級進程(LWP)的調度,實現了用戶態和內核態的協同分工。

每一個線程的TCB,在他創建時就已經在當前進程的堆空間上分配好空間了。

二、線程棧與線程局部存儲

剛剛說每個TCB中都記錄了當前線程獨立分配的棧空間。

沒錯,每個線程也會獨立分配棧空間信息,那么線程的棧與進程的棧有什么區別呢??

線程棧進程棧(主線程棧)
由線程庫(NPTL)通過?mmap?動態分配由內核在進程啟動時靜態分配
默認大小:8MB(可通過?pthread_attr_setstacksize?調整)

默認大小:8MB(受?ulimit -s?限制)

但是二者最大的區別,就是線程的棧,滿了之后是不會自動拓展的。但是進程的棧,是可能會自動拓展的。

子線程的棧原則上是他私有的,但是同一個進程的所有線程生成時,會淺拷貝生成這的task_struct的很多字段,所以如果愿意。其他線程是可以訪問到別人的棧區的。這一點我們在上一篇文章也提到過了。


我們之前說過,全局變量在多線程中是共享的,如果你改變了,我看見的也會改變。那有沒有什么辦法讓這個各自私有一份呢?

有的,就是線程局部存儲。

我們要用到__thread關鍵字。

#include <stdio.h>
#include<iostream>
#include <pthread.h>
#include <unistd.h>__thread int counter = 0;  // 每個線程有獨立副本void* thread_func(void* arg) 
{for (int i = 0; i < 3; i++) {counter++;  // 修改線程私有變量printf("Thread %ld: counter = %d (地址: %p)\n",(long)arg, counter, &counter);}return NULL;
}int main() 
{pthread_t t1, t2;pthread_create(&t1, NULL, thread_func, (void*)1);pthread_create(&t2, NULL, thread_func, (void*)2);pthread_join(t1, NULL);pthread_join(t2, NULL);printf("主線程: counter = %d\n", counter);  // 輸出0(主線程的獨立副本)return 0;
}

在全局變量前使用該關鍵字,可在各線程中私有一份,這個線程獨立存儲是在TCB中記錄的。


三、線程封裝

補充完了線程的知識,接下來我們就進行封裝一下我們的線程,方便后續課程的使用。

首先,我們要明確要實現的功能,

封裝幾個最常用的功能:

  • start():開新線程讓它跑起來

  • join():等這個線程干完活

  • detach():讓線程自己玩去,不用管它死活

  • stop():強行讓線程下崗(這個要小心用)

為了實現這些功能,我們就得想要哪些成員變量幫助我們實現方法,或者記錄一下信息:

  • 線程ID(_tid):不然找不到這個線程

  • 線程名(_name):調試時候好認人

  • 能不能join(_joinable):防止重復join搞出事情

  • 進程ID(_pid):這個可能有用先留著


所以我們可以先這樣寫:

#ifndef _MYTHREAD_HPP_
#define _MYTHREAD_HPP_#include<iostream>
#include<string>namespace ThreadModule
{class Mythread{public:Mythread(){}void start()//負責線程的創建{}void join()//負責線程的等待{}void stop()//負責線程的取消{}~Mythread(){}void detach()//負責線程的分離{}private:std::string _name;pthread_t _tid;pid_t _pid;bool _joinable;//判斷狀態,我們之前講了進程分離};
}#endif

為了后文我們方便調用測試方法,所以我們可以用function,來包含我們的方法(打印之類的),我們規定這個方法就是void(void)的函數,所以我們可以在類成員變量中新加一個方法,為了方便,可以使用重命名:using func_t = std::function<void()>;

另外,我們可以定義一個enum,來定義宏狀態來代表線程的運行狀態(不是分離):新建,運行,暫停

    using func_t = std::function<void()>;enum class TSTATUS{NEW,RUNNING,STOP};

想完這些,就是來實現我們的函數接口了。

首先是初始化,我們規定我們的線程要傳入相應的執行方法,所以構造函數需要外部傳入func_t類型。同時,為了方便從名稱看出來線程的數量等信息,我們可以在作用域中定義一個static int的number變量來記錄,在_name初始化時可以用上。

         Mythread(func_t func):_func(func), _status(TSTATUS::NEW), _joinable(true){_name = "Thread-" + std::to_string(number++);_pid = getpid();}

然后,就是創建進程,這里我們要注意,先檢查狀態是否為Running,如果是,就沒必要新建一個線程,

這里我們要注意的是,我們需要寫一個回調函數Routine,方便我們執行傳進來的函數func,以及改變運行狀態等操作,為了安全,這個回調函數應該寫在private中:

值得注意的是,我們Routine的前綴類型如果沒有加static,在我們start中pthread_create時會報錯。

因為我們的Routine是類成員函數,真正的函數參數中是有一個this指針的,所以我們這里必須加static限制。

     private:static void *Routine(void*args){Mythread *t = static_cast<Mythread *>(args);t->_status = TSTATUS::RUNNING;t->_func();return nullptr;}
         bool start()//負責線程的創建{if (_status != TSTATUS::RUNNING){int n = ::pthread_create(&_tid, nullptr, Routine, this); // TODOif (n != 0)return false;return true;}return false;}

順便,修改一下start函數返回類型為bool,為了方便我們獲取是否成功的信息。(這里是一切從簡了,否則我們還可以定義一個返回值錯誤的enum)


之后,就是對join,stop的封裝,實際上底層就是調用我們之前說過的pthread_cancel與pthread_join,所以這里不再過多贅述。

        bool join()//負責線程的等待{if (_joinable){int n = ::pthread_join(_tid, nullptr);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}bool stop()//負責線程的取消{if (_status == TSTATUS::RUNNING){int n = ::pthread_cancel(_tid);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}

最后,就是線程的分離,我們要判斷我們的成員變量_joinable的狀態,是否可以進行分離,隨后調用分離函數,最后再更新狀態:

        bool detach()//負責線程的分離{if (_joinable){int n = ::pthread_detach(_tid);if (n != 0)return false;_joinable = false;}}

為了方便我們后續的打印測試,所以我們可以新加一個name接口返回該線程的名字。

所以我們初代版本的簡單線程封裝,就已經完成了:

#ifndef _MYTHREAD_HPP_
#define _MYTHREAD_HPP_#include<iostream>
#include<string>
#include<functional>
#include<unistd.h>
#include<sys/types.h>namespace ThreadModule
{using func_t = std::function<void()>;static int number =1;enum class TSTATUS{NEW,RUNNING,STOP};class Mythread{private:static void *Routine(void*args){Mythread *t = static_cast<Mythread *>(args);t->_status = TSTATUS::RUNNING;t->_func();return nullptr;}public:Mythread(func_t func):_func(func), _status(TSTATUS::NEW), _joinable(true){_name = "Thread-" + std::to_string(number++);_pid = getpid();}bool start()//負責線程的創建{if (_status != TSTATUS::RUNNING){int n = ::pthread_create(&_tid, nullptr, Routine, this); // TODOif (n != 0)return false;return true;}return false;}bool join()//負責線程的等待{if (_joinable){int n = ::pthread_join(_tid, nullptr);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}bool stop()//負責線程的取消{if (_status == TSTATUS::RUNNING){int n = ::pthread_cancel(_tid);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}~Mythread(){}bool detach()//負責線程的分離{if (_joinable){int n = ::pthread_detach(_tid);if (n != 0)return false;_joinable = false;}}std::string Name(){return _name;}private:std::string _name;pthread_t _tid;pid_t _pid;bool _joinable;//判斷狀態,我們之前講了進程分離func_t _func;TSTATUS _status;};
}#endif

我們可以寫一些代碼來測試一下:
?

#include <stdio.h>
#include<iostream>
#include <pthread.h>
#include <unistd.h>#include "Mythread.hpp"int main()
{ThreadModule::Mythread t([](){while(true){std::cout << "hello world" << std::endl;sleep(1);}});t.start();std::cout << t.Name() << "is running" << std::endl;sleep(5);t.stop();std::cout << "Stop thread : " << t.Name()<< std::endl;sleep(1);t.join();std::cout << "Join thread : " << t.Name()<< std::endl;return 0;
}

那么如果我要用多線程呢?

我們這里不使用C++的方法可變參數,我們可以使用我們的老朋友容器來進行管理:
?


using thread_ptr_t = std::shared_ptr<ThreadModule::Mythread>;int main()
{std::unordered_map<std::string, thread_ptr_t> threads;// 如果我要創建多線程呢???for (int i = 0; i < 10; i++){thread_ptr_t t = std::make_shared<ThreadModule::Mythread>([](){while(true){//std::cout << "hello world" << std::endl;sleep(1);}});threads[t->Name()] = t;}for(auto &thread:threads){thread.second->start();std::cout<<thread.second->Name()<<"is started"<<std::endl;}sleep(5);for(auto &thread:threads){thread.second->stop();std::cout<<thread.second->Name()<<"is stopped"<<std::endl;}for(auto &thread:threads){thread.second->join();std::cout<<thread.second->Name()<<"is joined"<<std::endl;}return 0;
}

至此,一旦有了線程對象后,我們就能使用容器的方式對線程進行管理了,所以這就是:先描述再組織。

總結:

我們線程部分的第一階段的內容就到此結束了,接下來帶大家進入二階段:同步異步等概念知識的學習,屆時,我們就會接觸到鎖等概念了。

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

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

相關文章

【物理與機器學習】從非平衡熱力學到擴散模型

[toc] 0.引子:從非平衡熱力學開始 1.架構簡介 2.反向過程的具體推導與 DDPM 改進摘要&#xff1a;擴散模型將非平衡熱力學的“噪聲注入—去噪逆轉”理念注入生成建模中。DDPM&#xff08;Denoising Diffusion Probabilistic Models&#xff09;在 SD2015 的基礎上&#xff0c;通…

Git常用命令詳解:從入門到精通

前言 Git作為當今最流行的分布式版本控制系統&#xff0c;已經成為開發者必備的技能之一。無論你是獨立開發者還是團隊協作&#xff0c;掌握Git的基本操作都能極大提高工作效率。本文將詳細介紹Git的常用命令&#xff0c;幫助你快速上手并精通Git的基本使用。 一、Git基礎概念…

Vue-22-通過flask接口提供的數據使用plotly.js繪圖(一)

文章目錄 1 任務背景 2 Flask提供接口(server.py) 2.1 原始代碼 2.2 跨域問題 3 Vue3獲取數據并渲染Plotly圖表 3.1 新建工程 3.2 程序 3.2.1 index.html(入口) 3.2.2 cpmponents/Plot.vue(子組件) 3.2.3 App.vue(父組件) 3.2.4 main.ts 3.3 展示 4 選擇圖表類型繪圖 4.1 App.v…

【mysql】換主鍵

需求&#xff1a;曲庫表的主鍵錯了&#xff0c;原先設置的是(sang_id),應該設置為&#xff08;sang_name,singer&#xff09;聯合主鍵。-- &#xff08;0&#xff09;先備份數據&#xff0c;我這里沒備份 -- &#xff08;1&#xff09;進行主鍵的切換之前&#xff0c;要進行一些…

Redis原理之緩存

上篇文章&#xff1a; Redis原理之集群https://blog.csdn.net/sniper_fandc/article/details/149141342?fromshareblogdetail&sharetypeblogdetail&sharerId149141342&sharereferPC&sharesourcesniper_fandc&sharefromfrom_link 目錄 1 Redis作為MySQL…

關于集合的底層數據結構

單列集合Collection分為list集合和set集合list集合分為ArrayList和LinkedListArrayList--底層數據結構是數組1.通過索引查詢快2.增刪要重構索引,增刪慢 LinkedList--底層數據結構是鏈表1.無索引查詢慢2.通過改變前節點的尾指針和后節點的前指針指向可快速增刪,增刪快set集合(…

批量插入技巧:減少事務提交次數的性能提升

一、事務提交成本分析每次事務提交觸發?磁盤I/O同步?&#xff08;WAL機制&#xff09;、?日志寫入?和?鎖資源釋放?操作&#xff0c;高頻獨立提交會產生指數級開銷?。實驗表明&#xff1a;MySQL提交1萬次單條插入比單次批量插入?慢20倍以上??。高頻提交還加劇鎖競爭與…

importlib.import_module() 的用法與實戰案例

&#x1f31f; 一、什么是 importlib&#xff1f; importlib 是 Python 的一個內置標準庫&#xff0c;用于在程序運行時 動態導入模塊。 &#x1f524; 對比&#xff1a;普通 import vs importlib方式示例特點靜態導入import os編寫代碼時就確定要導入的模塊動態導入importlib.…

Oracle 12c 創建數據庫初級教程

1. 連接到Oracle sqlplus / as sysdba Oracle數據庫名稱默認為ORCL或sqlplus /ORCL as sysdba Oracle數據庫名稱默認為ORCL2. 創建表空間&#xff08;數據庫&#xff09; create user YOUR_USERNAME identified by "YOUR_PASSWORD"; YOUR_USERNAME為數據庫名稱和登…

zabbix服務器告警處理

zabbix服務器告警&#xff0c;信息為&#xff1a;Utilization of poller processes over 75%處理辦法為修改zabbix_server.conf配置文件&#xff0c;一般情況下為/etc/zabbix目錄下。根據自己輪詢器的類型修改對應的輪詢器的數量&#xff1b;我這里把StartPollers&#xff0c;S…

隨筆20250721 PostgreSQL實體類生成器

我來幫你創建一個C#程序&#xff0c;從PostgreSQL數據庫讀取表結構并生成對應的實體類文件。我已經創建了一個完整的PostgreSQL實體類生成器。這個程序包含以下主要功能&#xff1a;主要特性數據庫連接: 使用Npgsql連接PostgreSQL數據庫表結構讀取: 自動讀取所有表的結構信息類…

B樹、B-樹與B+樹

B樹、B-tree與B樹 在計算機科學&#xff0c;尤其是數據庫和文件系統的領域中&#xff0c;B樹、B-tree和B樹是理解數據如何被高效存儲和檢索的關鍵。它們之間關系緊密&#xff0c;但功能和應用上又存在著決定性的差異。 一、 核心概念澄清&#xff1a;B樹就是B-tree 首先需要明確…

視頻格式轉換工廠v3.2.5,集音視頻、圖片處理78MB

今天&#xff0c;我們要介紹的是一款功能強大的視頻處理軟件——視頻格式轉換工廠。這款軟件已經完美破解&#xff0c;無需登錄即可享受全部高級功能。它不僅支持視頻格式轉換&#xff0c;還涵蓋了音頻、圖片處理等多種功能&#xff0c;是一款真正的多媒體處理工具。 視頻格式轉…

VUE 中父級組件使用JSON.stringify 序列化子組件傳遞循環引用錯誤

背景 VUE 中父級組件使用JSON.stringify 序列化子組件傳遞的數據會報錯 runtime-core.esm-bundler.js:268 Uncaught TypeError: Converting circular structure to JSON –> starting at object with constructor ‘Object’ — property ‘config’ closes the circle 原因…

HTTP,HTTPS

在網絡工程師、開發工程師、運維工程師等崗位的面試中&#xff0c;??HTTP/HTTPS?? 是高頻必考知識點&#xff0c;尤其在前端、后端、測試、DevOps等與網絡通信相關的職位中。以下是系統化的核心考點梳理&#xff0c;涵蓋基礎概念、協議機制、安全特性及應聘高頻問題。??一…

Nginx訪問日志分析在云服務器環境的技術實現與案例

在云計算時代&#xff0c;Nginx訪問日志分析已成為服務器運維的關鍵環節。本文將深入解析如何通過日志切割、實時監控和可視化展示三大技術路徑&#xff0c;實現云環境下Nginx日志的高效分析。我們將結合具體案例&#xff0c;演示從原始日志到運維決策的完整技術閉環&#xff0…

鴻蒙實現一次上傳多張圖片

記錄初接觸鴻蒙&#xff0c;遇到的一個問題&#xff0c;需求是點擊一個圖片上傳的號圖&#xff0c;訪問本地圖片&#xff0c;可以選擇多張圖片并上傳。下面是圖片上傳后的方法&#xff1a;//選擇圖片并上傳private async showPhotoPicker() {const maxImageCount 3;const rema…

【STM32】CRC 校驗函數

先上一下 CRC校驗 的源代碼&#xff1a; void crc_check(unsigned char *ptr,unsigned int len) //crc為開源函數 {unsigned long wcrc0XFFFF;//預置16位crc寄存器&#xff0c;初值全部為1unsigned char temp;//定義中間變量int i0,j0;//定義計數for(i0;i<len;i)//循環計算每…

【Java】SVN 版本控制軟件的快速安裝(可視化)

目錄 一、SVN 的概述 1.1 SVN 的概念 1.2 SVN 與 Git 的對比 1.3 SVN 軟件 二、SVN 的安裝 2.1 SVN 的工作流程 2.2 服務器端 SVN 的安裝 三、SVN 服務器端的配置 3.1 搭建項目 3.2 權限控制 四、SVN 客戶端的配置 4.1 SVN 客戶端的下載 4.2 客戶端連接 SVN 服務器…

Hadoop安全機制深度剖析:Kerberos認證與HDFS ACL細粒度權限控制

Hadoop安全機制概述在大數據時代&#xff0c;Hadoop作為分布式計算框架的核心組件&#xff0c;其安全性直接關系到企業數據資產的保護。隨著數據價值的不斷提升&#xff0c;Hadoop安全機制已從早期的"簡單信任模式"演進為包含多重防護措施的綜合體系&#xff0c;其重…