目錄
前言
一、線程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:

#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(受? |
但是二者最大的區別,就是線程的棧,滿了之后是不會自動拓展的。但是進程的棧,是可能會自動拓展的。
子線程的棧原則上是他私有的,但是同一個進程的所有線程生成時,會淺拷貝生成這的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;
}
至此,一旦有了線程對象后,我們就能使用容器的方式對線程進行管理了,所以這就是:先描述再組織。
總結:
我們線程部分的第一階段的內容就到此結束了,接下來帶大家進入二階段:同步異步等概念知識的學習,屆時,我們就會接觸到鎖等概念了。