Linux操作系统之线程(五):线程封装
目录
前言
二、线程栈与线程局部存储
三、线程封装
总结:
前言
我们在上篇文章着重给大家说了一下线程的控制的有关知识。
但是如果我们要使用线程,就得那这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 #include#include #include void* thread_func(void* arg) { // 获取当前线程的 pthread_t pthread_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_t std::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中都记录了当前线程独立分配的栈空间。
没错,每个线程也会独立分配栈空间信息,那么线程的栈与进程的栈有什么区别呢?
mmap
动态分配pthread_attr_setstacksize
调整)默认大小:8MB(受 ulimit -s
限制)
但是二者最大的区别,就是线程的栈,满了之后是不会自动拓展的。但是进程的栈,是可能会自动拓展的。
子线程的栈原则上是他私有的,但是同一个进程的所有线程生成时,会浅拷贝生成这的task_struct的很多字段,所以如果愿意。其他线程是可以访问到别人的栈区的。这一点我们在上一篇文章也提到过了。
我们之前说过,全局变量在多线程中是共享的,如果你改变了,我看见的也会改变。那有没有什么办法让这个各自私有一份呢?
有的,就是线程局部存储。
我们要用到__thread关键字。
#include #include#include #include __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#includenamespace 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;
另外,我们可以定义一个enum,来定义宏状态来代表线程的运行状态(不是分离):新建,运行,暂停
using func_t = std::function; 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(args); t->_status = TSTATUS::RUNNING; t->_func(); return nullptr; }
bool start()//负责线程的创建 { if (_status != TSTATUS::RUNNING) { int n = ::pthread_create(&_tid, nullptr, Routine, this); // TODO if (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#include#include#include#includenamespace ThreadModule{ using func_t = std::function; static int number =1; enum class TSTATUS { NEW, RUNNING, STOP }; class Mythread { private: static void *Routine(void*args) { Mythread *t = static_cast(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); // TODO if (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 #include#include #include #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;int main(){ std::unordered_map threads; // 如果我要创建多线程呢??? for (int i = 0; i < 10; i++) { thread_ptr_t t = std::make_shared([](){ while(true) { //std::cout << \"hello world\" <Name()] = t; } for(auto &thread:threads) { thread.second->start(); std::cout<Name()<<\"is started\"<stop(); std::cout<Name()<<\"is stopped\"<join(); std::cout<Name()<<\"is joined\"<<std::endl; } return 0;}
至此,一旦有了线程对象后,我们就能使用容器的方式对线程进行管理了,所以这就是:先描述再组织。
总结:
我们线程部分的第一阶段的内容就到此结束了,接下来带大家进入二阶段:同步异步等概念知识的学习,届时,我们就会接触到锁等概念了。