【Linux 篇】数字世界的线程工坊 —— Linux 线程控制与自研线程库深度实战解析

发布时间:2026/8/30 5:52:49
【Linux 篇】数字世界的线程工坊 —— Linux 线程控制与自研线程库深度实战解析 于此止个人主页本文选自专栏 《Linux篇》❄️个人专栏传送门 《QT百日筑基篇》 《算法篇》 《Linux网络篇》《C与STL剖析篇》 《数据结构篇》 《Python基础篇》《嵌入式砺剑录》⭐️纵有狂风拔地起我亦乘风破万里⭐️Linux线程控制和封装Pthread库前言看本文章之前我们要先学习我们前置的知识我们基于前置知识我们了解我们对应的线程传送门【Linux 篇】数字世界的任务执行者 —— Linux 线程概念与虚拟物理内存页表映射深度实战解析-CSDN博客我们本文主要是认识我们的线程接口和我们封装我们的线程我们的封装按照我们的库的形式进行封装目录/索引目录Linux线程控制和封装Pthread库目录/索引一、Linux线程控制1.POSIX线程库pthread_create:创建线程ps -aL:查看我们的线程线程的崩溃phread_self 获取线程的id信息。线程的终止pthread_exit: 终止自己的线程强制pthread_cancel:终止指定的进程请求pthread_join线程的等待pthread_detach:分离我们的线程。二、线程ID级进程地址空间的空间布局线程 ID 及进程地址空间布局三、线程库的封装一、Linux线程控制1.POSIX线程库与线程有关的函数构成了一个完整的系列绝大多数函数的名字都是以pthread_开头的。要使用这些函数库需要引入我们的头文件pthread.h链接这个文件这个文件属于第三方库我们要在编译的时候要加入我们的”-lpthread“选项。pthread_create:创建线程我们依旧先看我们的官方文档。man pthread_createNAME pthread_create - create a new thread SYNOPSIS #include pthread.h int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);我们能看到我们使用之前要包含我们对应的头文件pthread.h参数的介绍1.pthread_t* thread:输出型参数我们这里看到我们要传一个pthread_t 指针那么这和pthread_t这个是什么呢我们来看一看吧我们的这个是一个无符号的长整形。所以我们这里要先创建一个ptread_t的变量这里我们称之为新线程的id。2.const pthread_attr_t *attr这个参数是我们的设置我们线程的属性我们一般设置为NULL表示默认的属性。3.void *(*start_routine) (void *)我们学过我们的c语言我们一定能看出来这是一个函数指针我们后续要穿一个函数指针让我们的创建的新线程去执行这个函数。我们的主线成继续的向下进行执行。注意我们的参数是我们的void* 如果我们想返回什么内容要强转成void* 。本质就是一个回调函数机制。4.void *arg我们第三个参数所传的参数注意这个参数是我们的void*类型的我们传一个字符串的时候我们要将它进行强制类型的转化噢。返回值成功返回0失败返回错误码。错误检查:传统的一些函数是成功返回 0失败返回 - 1并且对全局变量 errno 赋值以指示错误。pthreads 函数出错时不会设置全局变量 errno而大部分其他 POSIX 函数会这样做。而是将错误代码通过返回值返回pthreads 同样也提供了线程内的 errno 变量以支持其它使用 errno 的代码。对于 pthreads 函数的错误建议通过返回值判定因为读取返回值要比读取线程内的 errno 变量的开销更小那我们下面创建一个线程来看看结果。注意我们写好的代码编译后要加入-l pthread。这是我们自己实现的第三方库。test:pthread_test.cc g -o $ $^ -l pthread echo 编译成功 .PHONY:clean clean: rm -f test echo test 被删除这样我们就创建了一个新线程。#includeiostream #includepthread.h #includecstdio void* Print(void*s) { printf(%s\n,(char*)s); return (void*)print success; //pthread_exit((void*)print success); } int main() { //创建我们的线程make pthread_t tid; int ret pthread_create(tid,nullptr,Print,(void*)我是一个新线程); if(ret ! 0) { perror(pthread_creat fail!!!); exit(1); } printf(我是一个主线程\n); return 0; }我们可以sleep几秒查看我们的细节。首先我们先认识一个命令这个是我们查看我们的轻量级进程的也就是我们的线程ps -aL:查看我们的线程我们的PID是我们当前进程的pid我们的LWP(Light weight process轻量级进程)也就是我们的线程的id信息。到这了我们有个疑问我们的LWP是我们程序定义的时候的thread吗我们将我们的程序的id打印看看吧。我们惊讶的看到我们的pid是不一样的这正常吗当然正常啊。怎么正常我的LWP是我们的系统的概念他是一个轻量级进程不是我们的线程我们的操作系统可以说没有我们的线程我们的线程是用我们的轻量级进程进行实现的。我们的pthread——create是我们c语言库提供的既然是我们c语言库提供的我们当然要搞一套我们自己线程的id了。所以我们的两个的id是不一样的。打印出来的 tid 是通过 pthread 库中有函数pthread_self得到的它返回一个 pthread_t 类型的变量指代的是调用 pthread_self 函数的线程的 ID。怎么理解这个 ID 呢这个 ID 是 pthread 库给每个线程定义的进程内唯一标识是 pthread 库维持的。由于每个进程有自己独立的内存空间故此ID 的作用域是进程级而非系统级内核不认识。其实 pthread 库也是通过内核提供的系统调用例如 clone来创建线程的而内核会为每个线程创建系统全局唯一的 ID 来唯一标识这个线程。lwp用户不关心我们的pthread_creat是封装了我们的vfork() 和我们close()的这两个就是创建我们的轻量级进程的。Linux线程的实现是用户实现出来的我们称之为用户级线程。这里我们看到我们的代码时先创建我们的主线程然后我们经过2s后创建我们的新线程然后再过2s我们的两个线程都退出了。纯净版的控制循环打印脚本。while :; do ps -aL | grep -v ps | grep -v grep; sleep 1; done#includeiostream #includepthread.h #includecstdio #include unistd.h void* Print(void*s) { sleep(2); printf(%s\n,(char*)s); return (void*)print success; //pthread_exit((void*)print success); } int main() { //创建我们的线程make pthread_t tid; std::cout线程的id信息tid std::endl; sleep(2); int ret pthread_create(tid,nullptr,Print,(void*)我是一个新线程); sleep(2); if(ret ! 0) { perror(pthread_creat fail!!!); exit(1); } printf(我是一个主线程\n); return 0; }线程的崩溃我们上一篇文章说过我们的进程崩溃了我们的线程也就崩溃了我们的线程崩溃了我们的进程是不是也会崩溃呢我们试一试吧。我们这里直接的退出我们的主线程下面还没来的急打呢我的线程就退出了。所以我们的子线程异常退出我们的进程也会直接的退出的。下面我们在介绍我们的一个函数用来获取我们的线程id的。phread_self 获取线程的id信息。man pthread_self函数原型NAME pthread_self - obtain ID of the calling thread SYNOPSIS #include pthread.h pthread_t pthread_self(void);返回值函数不会失败返回我们线程的id信息。使用是当然这也可能会出现我们的信息打在一起的情况我们后期学了互斥就好了。LWP 是什么呢LWP 得到的是真正的线程 ID。之前使用pthread_self得到的这个数实际上是一个地址在虚拟地址空间上的一个地址通过这个地址可以找到关于这个线程的基本信息包括线程 ID线程栈寄存器等属性。在ps -aL得到的线程 ID有一个线程 ID 和进程 ID 相同这个线程就是主线程主线程的栈在虚拟地址空间的栈上而其他线程的栈在是在共享区堆栈之间因为 pthread 系列函数都是 pthread 库提供给我们的。而 pthread 库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。当然我们线程创建了我们要怎么终止我们的线程呢线程的终止当然我们也有对应的线程终止的函数调用。如果只终止某个线程不终止整个进程可以有三种方法1.从线程函数return 。这种方法对于主线程不适用从main函数调用return相当于终止整个进程。2.线程可以调用自己的pthread_exit库来终止自己。3.一个线程可以调用自己的pthread_cancel来终止同一个进程中的另一个线程。我们先看pthread_exit: 终止自己的线程强制man pthread_exit函数原型NAME pthread_exit - terminate calling thread SYNOPSIS #include pthread.h void pthread_exit(void *retval);terminate结束。结束一个线程。参数解析retval不要指向我们的一个局部变量因为我们这个线程被结束了的时候我们的局部变量都会被释放所以我们带出去会访问野指针的所以我们一般返回我们的字符串当作我们的取消信息。返回值我们线程退出了没有有谁能接受返回值所以我们没有返回值我们下面使用了进程的等待我们下面会讲解的。我只是先把我们的消息打印出来。方便观察。#includeiostream #includepthread.h #includecstdio #include unistd.h void* Print(void*s) { //sleep(2); printf(%s\n,(char*)s); pthread_t id pthread_self(); std::cout新线程的id信息 id std::endl; //return (void*)print success; pthread_exit((void*)exit success); //return (void*)print success; } int main() { //创建我们的线程make pthread_t tid; //sleep(2); int ret pthread_create(tid,nullptr,Print,(void*)我是一个新线程); sleep(2); if(ret ! 0) { perror(pthread_creat fail!!!); exit(1); } printf(我是一个主线程\n); pthread_t id pthread_self(); std::cout主线程的id信息id std::endl; std::cout主线程的tid信息tid std::endl; //等待我们的新线程 void* retval; ret pthread_join(tid,retval); if(ret!0) { perror(pthread_join fail!); exit(2); } printf(%s\n, (char*)retval); return 0; }pthread_cancel:终止指定的进程请求man pthread_cancel函数原型NAME pthread_cancel - send a cancellation request to a thread SYNOPSIS #include pthread.h int pthread_cancel(pthread_t thread);send a cancellation request to a thread发送一个请求取消这个线程。注意我们只是请求并不是一定。参数介绍thread我们能看出来我们的thread是一个我们的一个id我们传入我们的一个id我们对应的线程就会被取消。返回值成功返回我们的0失败返回非0的错误码。函数使用#includeiostream #includepthread.h #includecstdio #include unistd.h void* Print(void*s) { sleep(2); printf(%s\n,(char*)s); pthread_t id pthread_self(); std::cout新线程的id信息 id std::endl; //return (void*)print success; //pthread_exit((void*)exit success); return (void*)print success; } int main() { //创建我们的线程make pthread_t tid; //sleep(2); int ret pthread_create(tid,nullptr,Print,(void*)我是一个新线程); //sleep(2); if(ret ! 0) { perror(pthread_creat fail!!!); exit(1); } printf(我是一个主线程\n); pthread_t id pthread_self(); std::cout主线程的id信息id std::endl; std::cout主线程的tid信息tid std::endl; //线程的终止 ret pthread_cancel(tid); if(ret!0) { perror(pthread_cancel fail!); exit(3); } printf(线程%ld已被终止\n, tid); return 0; }pthread_join线程的等待为什么我们需要等待我们的线程呢1.已退出的线程其空间没有被释放仍在该进程的地址空间内。2.创建新的线程不会复用刚退出的线程地址空间。我们先查看文档介绍man pthread_join函数原型NAME pthread_join - join with a terminated thread SYNOPSIS #include pthread.h int pthread_join(pthread_t thread, void **retval);这里我们有两个参数1.thread我们要等待哪个进程我需要该进程的tid2.retval我们的retval是一个二级指针是为了接受我们的返回值。我们线程的返回值是一个void*所以我们要使用二级指针来进行接收。输出型参数我们不关心设置为NULL。返回值成功返回0失败返回-1.代码测试上面已经写过了。调用该函数的线程将挂起等待直到 id 为 thread 的线程终止。thread 线程以不同的方法终止通过 pthread_join 得到的终止状态是不同的总结如下:如果 thread 线程通过 return 返回value_ptr 所指向的单元里存放的是 thread 线程函数的返回值。如果 thread 线程被别的线程调用 pthread_cancel 异常终掉value_ptr 所指向的单元里存放的是常数 PTHREAD_CANCELED。如果 thread 线程是自己调用 pthread_exit 终止的value_ptr 所指向的单元存放的是传给 pthread_exit 的参数。如果对 thread 线程的终止状态不感兴趣可以传 NULL 给 value_ptr 参数。我们这里进行下面的几组测试1.调用我们的pthread_exit和pthread_join的情况下我们先调用我们的pthread_exit后调用pthread_join 的情况如下正常运行。1.调用我们的pthread_exit和pthread_join的情况下我们先调用我们的pthread_join后调用pthread_exit 的情况如下正常运行3.调用我们的pthread_cancel和pthread_join的情况下我们先调用我们的pthread_cancel后调用pthread_join 的情况如下正常运行我测试了很多组得到一个结论我们的pthread_cancel可能会出现段错误。下面解释注意点我们的pthread_exit 和我们pthread_join 一起使用是没什么问题的但是我们的ptread_join 和我们的pthread_cancel是有问题的原因是我们先发送我们的信息我们该函数没有返回值没有运行到返回所以我们接受不到我们的返回信息那么在这里我们就不能打印我们的信息因为我们的ret参数没有收到我们对应的返回值。如果我们的retval我们的PTHREAD_CANCELED那就说明我们的线程已经被取消了我们没拿到对应的返回值。pthread_detach:分离我们的线程。默认情况下新创建的线程是 joinable 的线程退出后需要对其进行 pthread_join 操作否则无法释放资源从而造成系统泄漏。如果不关心线程的返回值join 是一种负担这个时候我们可以告诉系统当线程退出时自动释放线程资源。我们查阅一下我们的官方文档man pthread_detach函数原型NAME pthread_detach - detach a thread SYNOPSIS #include pthread.h int pthread_detach(pthread_t thread);参数的简介thread:传入我要分离的进程id。分离状态线程结束后我们直接进行释放不需要我们在手动的进行join。线程的分离主线程能分离新线程主线程也能分离主线程。我们线程处于我们的分离状态不能在使用joinjoin会失败。返回值成功返回0失败返回错误码。#includeiostream #includepthread.h #includecstdio #include unistd.h void* Print(void*s) { sleep(5); printf(%s\n,(char*)s); pthread_t id pthread_self(); std::cout新线程的id信息 id std::endl; //return (void*)print success; //pthread_exit((void*)exit success); return (void*)print success; } int main() { //创建我们的线程make pthread_t tid; //sleep(2); int ret pthread_create(tid,nullptr,Print,(void*)我是一个新线程); //sleep(2); if(ret ! 0) { perror(pthread_creat fail!!!); exit(1); } printf(我是一个主线程\n); pthread_t id pthread_self(); std::cout主线程的id信息id std::endl; std::cout主线程的tid信息tid std::endl; //sleep(5); //线程的终止 ret pthread_cancel(tid); if(ret!0) { perror(pthread_cancel fail!); exit(3); } printf(线程%ld已被终止\n, tid); //线程的分离 ret pthread_detach(tid); if(ret !0) { perror(pthread_detach fail!); exit(4); } printf(线程已被分离\n); return 0; }二、线程ID级进程地址空间的空间布局线程 ID 及进程地址空间布局pthread_create函数会产生一个线程 ID存放在第一个参数指向的地址中。该线程 ID 和前面说的线程 ID 不是一回事。前面讲的线程 ID 属于进程调度的范畴。因为线程是轻量级进程是操作系统调度器的最小单位所以需要一个数值来唯一表示该线程。pthread_create函数第一个参数指向一个虚拟内存单元该内存单元的地址即为新创建线程的线程 ID属于 NPTL 线程库的范畴。线程库的后续操作就是根据该线程 ID 来操作线程的。线程库 NPTL 提供了pthread_self函数可以获得线程自身的 ID三、线程库的封装#include iostream #include pthread.h #include cstring #includefunctional #include cstdio #include unistd.h #define TID_FILE 1 using Func std::functionvoid(); namespace nThread { static int count 1; class Thread { private: void Enablerunning() { _runningtrue; } void NoEnablerunning() { _runningfalse; } void NoEnabledetach() { _detachfalse; } void Enabledetach() { _detachtrue; } static void* Running_func(void* argv) { Thread* self static_castThread* (argv); self-Enablerunning(); pthread_setname_np(self-_tid,self-_name.c_str()); if(self-_detach) self-NoEnabledetach(); self-_func(); return nullptr; } public: Thread(Func func) :_tid(TID_FILE), _running(false), _detach(true),_ret(nullptr), _func(func) { _name 我是线程 std::to_string(count); } //启动线程 bool Start() { //创建线程 if(_running) return false; int n pthread_create(_tid,NULL,Running_func,this); printf(线程的id是 %ld,_tid); if(n ! 0) { perror(pthread fail!!!); return false; } //创建成功 std::cout 线程创建成功 std::endl; return true; } //线程的等待 void* Join() { if(_detach) return nullptr; int ret pthread_join(_tid,_ret); if(ret ! 0) { perror(pthread_join fail!!!); exit(1); } printf(线程等待成功\n); //NoEnablerunning(); return _ret; } //线程的取消 bool Cancel() { if(!_running) return false; if(pthread_cancel(_tid)!0) { perror(pthread_cancel fail!!!); exit(1); } NoEnablerunning(); std::cout线程已被取消std::endl; return true; } //线程的分离 void Detach() { if(_detach) return; Enabledetach(); if(!_running) return; if(pthread_detach(_tid)!0) { perror(pthread_detach fail!!!); exit(1); } std::cout线程成功进行分离std::endl; } void Getname(char* buffer) { if(_running) pthread_getname_np(_tid,buffer,sizeof(buffer)); else std::cout线程已经结束无法获取namestd::endl; } ~Thread(){} private: pthread_t _tid; std::string _name; void* _ret; bool _running; bool _detach; Func _func; }; }