【Linux】从零开始认识多线程 --- 线程控制
发布时间 - 2025-07-18 00:00:00 点击率:次零开始认识多线程 --- 线程控制
1 知识回顾2 线程控制2.1 线程创建2.2 线程等待2.3 线程终止3 测试运行3.1 小试牛刀 --- 创建线程3.2 探幽析微 --- 理解线程参数3.3 小有心得 --- 探索线程返回3.4 求索无厌 --- 实现多线程3.5 返璞归真 --- 线程终止与线程分离4 语言层的线程封装Thanks♪(・ω・)ノ谢谢阅读!!!下一篇文章见!!!1 知识回顾上一篇文章中,我们通过对地址空间的再次学习来认识了线程:
物理空间不是连续的,是4kb的内存块(页框)组成的。页表映射是通过虚拟地址来索引物理地址: 虚拟地址共32位:前10位用来索引页目录中的元素(页表),中间10位用来索引页表中的对应的元素(页框),后12位用来索引页框中的每一个字节虚拟地址本质是一种资源,可以进行分配!对一个进程的数据进行分配执行,就是多线程的本质!Linux中的线程是通过进程模拟的(并没有单独设计出一个单独的线程模块)进程中可以有多个进程(之前学习的是进程的特殊情况),他们共用一个地址空间。进程从内核来看,是承担分配系统资源的基本实体!Linux中的执行流是线程 ,CPU看到的执行流 进程与线程需要注意:线程的调度成本比进程低很多,是由于硬件原因:CPU中存在一个c++ache会储存热点数据(进程相关数据) ,要访问数据时,会先在cache中寻找,如果命中直接访问,反之进行置换。切换进程需要更换热点数据,切换线程不需要切换。线程的健壮性很差!一个线程出错会导致整个线程退出,而不同进程是独立的互不影响!进程和线程各有特长!线程的本质是代码块!只使用函数的对应代码,即拿页表的一部分来执行!!!线程的使用场景多为计算密集型和IO密集型,可以充分使用CPU的并行能力!同一个进程中的线程虽然共享一个地址空间,但是还是有独属于自己的一些东西:
一组寄存器:在硬件中储存上下文数据,保证线程可以动态并行运行!栈空间:线程中可以处理自己的临时变量,临时变量储存在自己独立的栈区,可以独立完成任务。线程IDerrno信号屏蔽字调度优先级复习的差不多了,我们了解了线程的基本概念,接下来就要开始学习如何管理线程 — 线程控制。根据我们之前学习的进程控制,大概可以估计一下线程控制的基本接口:线程创建 , 线程等待 , 线程退出…
2 线程控制2.1 线程创建万事开头难,我们先来看线程怎么创建:
代码语言:javascript代码运行次数:0运行复制PTHREAD_CREATE(3) Linux Programmer's Manual PTHREAD_CREATE(3)NAME pthread_create - create a new threadSYNOPSIS #includeint pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); Compile and link with -pthread.
pthread_create是创建线程的接口,里面有4个参数:
再来看返回值:
代码语言:javascript代码运行次数:0运行复制RETURN VALUE On success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.
pthread系列的函数的返回值是都是一样的:成功返回0,反之返回错误码!
学习进程的时候,如果进程创建出来了,但是不进行等待,就拿不到退出信息,还会造成僵尸进程,进而造成内存泄漏。同样线程也需要进行等待。由主线程来等待新线程
代码语言:javascript代码运行次数:0运行复制PTHREAD_JOIN(3) Linux Programmer's Manual PTHREAD_JOIN(3)NAME pthread_join - join with a terminated threadSYNOPSIS #includeint pthread_join(pthread_t thread, void **retval); Compile and link with -pthread.
这个函数里面有2个参数:
pthread_t thread:需要进行等待的线程IDvoid **retval: 获取的返回信息2.3 线程终止牢记:main线程结束那么进程结束,所以一定要保证main线程最后退出。
最简单的线程终止是线程函数返回return !切记不要使用exit(),我们在进程控制中学习过exit()可以退出进程,但是要注意线程是在一个进程中讨论的,新线程如果使用了exit()那整个进程就退出了!exit()不可以用来终止线程操作系统也给我们提供了线程终止的接口:代码语言:javascript代码运行次数:0运行复制PTHREAD_CANCEL(3) Linux Programmer's Manual PTHREAD_CANCEL(3)NAME pthread_cancel - send a cancellation request to a threadSYNOPSIS #includeint pthread_cancel(pthread_t thread); Compile and link with -pthread.
通过这个参数,可以看出来这是个很简单的接口,终止对应tid的线程。只要线程存在,并且知道tid , 就可以终止线程(可以自己终止自己)。线程终止的返回值是一个整数!
3 测试运行3.1 小试牛刀 — 创建线程我们进行一个简单的测试,来使用这两个接口: 注意,使用线程库的接口需要动态链接g++ -o @ ^ -std=c++11 -lpthread
代码语言:javascript代码运行次数:0运行复制#include#include #include #include #include // 测试 1void *ThreadRun(void *args){ std::cout << "name: " << *(std::string*)args << " is running"<< std::endl; sleep(1); std::string* ret = new std::string(*(std::string*)args + "finish...") ; return (void*)ret;}int main(){ // 创建一个新线程 // int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); pthread_t tid; std::string name = "thread - 1"; pthread_create(&tid, nullptr, ThreadRun, &name); //进程等待 //int pthread_join(pthread_t thread, void **retval); std::string *ret = nullptr; pthread_join(tid, (void**)&ret); std::cout << *(std::string*)ret << std::endl; return 0;}
编译运行一下,我们可以看到:
新线程完成了任务!
问题 1 : main线程和new线程谁先运行? 不确定,和进程的调度方式一致,由具体情况来定。
问题 2 : 我们期望谁先退出?肯定是main线程,所以就有join来进行等待,阻塞等待线程退出。如果不进行join,就会造成类似僵尸进程的情况(内存泄漏)!
问题 3 :tid是什么样子的,我们可不可以看一看?当然可以:
代码语言:javascript代码运行次数:0运行复制std::string ToHex(int x){ char buffer[128]; snprintf(buffer, sizeof(buffer), "0x%x", x); return buffer;}这样就可以打印出来tid的十六进制:
这数字好像和lwp不一致啊
为什么tid这么大?其实tid是一个虚拟地址!!!
问题 4 : 全面看待线程函数传参。上面我们的程序传入了name变量的地址,让线程获取了对应的名字。如果想要传入多个变量或方法,可以传入类对象的地址:
class ThreadData{public: std::string name; int num;};vvoid *ThreadRun(void *args){ ThreadData* td = static_cast(args); std::cout << "name: " << td->name << " is running" << std::endl; std::cout << "num: " << td->num << std::endl; sleep(1); std::string *ret = new std::string(*(std::string *)args + "finish..."); return (void *)ret;}std::string ToHex(int x){ char buffer[128]; snprintf(buffer, sizeof(buffer), "0x%x", x); return buffer;}int main(){ // 创建一个新线程 // int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); pthread_t tid; // std::string name = "thread - 1"; ThreadData td; td.name = "thread - 1"; td.num = 100; pthread_create(&tid, nullptr, ThreadRun, &td); // 查看tid sleep(1); std::cout << "tid: " << ToHex(tid) << std::endl; // 进程等待 // int pthread_join(pthread_t thread, void **retval); std::string *ret = nullptr; pthread_join(tid, (void **)&ret); std::cout << *(std::string *)ret << std::endl; return 0;} 这样就可以传入多个变量:
所以这个void*的变量是可以传入任何地址的,一定要想到可以传入类对象。但是刚写的有些问题,我们上面的写法是在主线程的栈区创建变量,让新线程读取主线程的栈,不太合适(破坏了一定独立性)!如果多个变量都传入了这个变量,那么修改一个就会造成所以的线程中的数据都发生改变!!!这可不行!推荐写:
ThreadData* td = new ThreadData(); td->name = "thread - 1"; td->num = "100"; pthread_create(&tid, nullptr, ThreadRun, td);
这是在堆区进行开辟空间,然后将该空间交给新线程来管理!就不会出现这样的问题了!以后我们都使用这种方式来传递参数!!!
3.3 小有心得 — 探索线程返回问题 5 :线程的返回值输出型参数void** retval,他需要我们传递一个void*变量,然后返回值就交给了void*变量!这个过程就是对一个指针进行改变其指向的内容的操作。
下面是一个让新线程进行加法工作的程序
代码语言:javascript代码运行次数:0运行复制void *ThreadRun(void *args){ ThreadData* td = static_cast(args); std::cout << "name: " << td->name << " is running" << std::endl; std::cout << "num: " << td->num << std::endl; sleep(1); delete td; //返回值 std::string *ret = new std::string(*(std::string *)args + "finish..."); return (void *)ret;} 这就将void*变量返回给&(void* ret)变量,让ret指向对应的堆区。这就类似int a放入int * 中就可以改变a的值
问题 5 :如何全面的看待线程的返回。我们知道如果一个线程出现问题,整个进程就会退出。所以线程的返回只有正常的返回,没有异常的返回,出现异常整个进程会直接退出,根本没有返回错误信息的机会!和传入参数音参数一样,我们也可以返回一个类对象来传递多个变量。
代码语言:javascript代码运行次数:0运行复制#include#include #include #include #include // 测试 1class ThreadData{public: std::string name; int num1; int num2;};class ThreadResult{public: std::string name; int num1; int num2; int ans;};void *ThreadRun(void *args){ ThreadData *td = static_cast (args); std::cout << "name: " << td->name << " is running" << std::endl; std::cout << "num1: " << td->num1 << " num2: " << td->num2 << std::endl; sleep(1); ThreadResult *ret = new ThreadResult(); ret->name = td->name; ret->num1 = td->num1; ret->num2 = td->num2; ret->ans = td->num2 + td->num1; delete td; return (void *)ret;}std::string ToHex(int x){ char buffer[128]; snprintf(buffer, sizeof(buffer), "0x%x", x); return buffer;}int main(){ // 创建一个新线程 // int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); pthread_t tid; // std::string name = "thread - 1"; ThreadData *td = new ThreadData(); td->name = "thread - 1"; td->num1 = 100; td->num2 = 88; pthread_create(&tid, nullptr, ThreadRun, td); // 查看tid sleep(1); std::cout << "tid: " << ToHex(tid) << std::endl; // 进程等待 // int pthread_join(pthread_t thread, void **retval); ThreadResult *ret = nullptr; pthread_join(tid, (void **)&ret); std::cout << ret->num1 << " + " << ret->num2 << " = " << ret->ans << std::endl; return 0;}
来看返回值:
我们成功获取了新线程中设置的返回值!非常nice!
3.4 求索无厌 — 实现多线程问题 6 :上面只是创建了单独的一个线程,那如何创建多线程呢? 可以通过维护一个vector数组来对tid进行统一管理
代码语言:javascript代码运行次数:0运行复制void *ThreadRun(void *args){ std::string name = static_cast(args); while (true) { std::cout << name << "is running ..." << std::endl; sleep(1); } return (void *)0;}std::string ToHex(int x){ char buffer[128]; snprintf(buffer, sizeof(buffer), "0x%x", x); return buffer;}const int num = 10;int main(){ std::vector tids; for (int i = 0; i < num; i++) { // 1. 线程ID pthread_t tid; // 2. 线程名字 char* name = new char[128]; snprintf(name, 128, "thread - %d", i + 1); pthread_create(&tid, nullptr, ThreadRun, name); //保存所有线程的ID tids.push_back(tid) ; } //join sleep(100); return 0;} 这样就创建出了10个新线程,但是我们看这些新线程的的名字好像不太对:
怎么不是1 - 10???完全是乱的!因为线程谁先被调度运行不确定!而我们传入的名字是在主线程的栈区域,可能在新线程还没有调度,name就已经在主线程中被覆盖了!解决办法很简单,我们创建在堆区就可以了
代码语言:javascript代码运行次数:0运行复制for (int i = 0; i < num; i++) { // 1. 线程ID pthread_t tid; // 2. 线程名字 //在堆区进行创建。防止被重写覆盖 char* name = new char[128]; snprintf(name, 128, "thread - %d", i + 1); pthread_create(&tid, nullptr, ThreadRun, name); pids.push_back(tid) ; }这样就整齐多了!
接下来就要进行等待: 我们已经通过vector容器来维护了创建所有线程的tid,所以只需要对所有的tid进行join就好了!
代码语言:javascript代码运行次数:0运行复制void *ThreadRun(void *args){ std::string name = static_cast(args); while (true) { std::cout << name << "is running ..." << std::endl; sleep(3); break; } return nullptr;}std::string ToHex(int x){ char buffer[128]; snprintf(buffer, sizeof(buffer), "0x%x", x); return buffer;}const int num = 10;int main(){ std::vector tids; for (int i = 0; i < num; i++) { // 1. 线程ID pthread_t tid; // 2. 线程名字 char* name = new char[128]; snprintf(name, 128, "thread - %d", i + 1); pthread_create(&tid, nullptr, ThreadRun, name); //保存所有线程的ID tids.push_back(tid) ; } //join for (auto tid : tids) { pthread_join(tid , nullptr); std::cout << ToHex(tid) << " quit..." << std::endl; }} 来看运行效果:
非常好!!!
我们也可以通过返回值来获取线程的名字:
代码语言:javascript代码运行次数:0运行复制 for (auto tid : tids) { void* name = nullptr; pthread_join(tid , &name); std::cout << (const char*)name<< " quit..." << std::endl; delete (const char*)name; }非常优雅!
问题 7 :线程终止的返回值 我们来看看通过线程终止接口终止的线程返回值是什么样的:
代码语言:javascript代码运行次数:0运行复制void *ThreadRun(void *args){ std::string name = static_cast(args); while (true) { std::cout << name << "is running ..." << std::endl; sleep(3); //break; } return args;}std::string ToHex(int x){ char buffer[128]; snprintf(buffer, sizeof(buffer), "0x%x", x); return buffer;}const int num = 10;int main(){ std::vector tids; for (int i = 0; i < num; i++) { // 1. 线程ID pthread_t tid; // 2. 线程名字 char* name = new char[128]; snprintf(name, 128, "thread - %d", i + 1); pthread_create(&tid, nullptr, ThreadRun, name); //保存所有线程的ID tids.push_back(tid) ; } //join sleep(3); for (auto tid : tids) { pthread_cancel(tid); std::cout << " cancel: " << ToHex(tid) << std::endl; void* ret= nullptr; pthread_join(tid , &ret); std::cout << (long long int)ret << " quit..." << std::endl; } return 0;} 可以看的,被phread_cancel()终止的线程的返回值是 -1!这个 -1其实是宏定义#define PTHREAD_CANCELED ((void *) -1)。线程终止的方式有三种:
问题 8 :可不可以不通过join线程,让他执行完就退出呢,当然可以! 这里需要线程分离接口:
代码语言:javascript代码运行次数:0运行复制PTHREAD_DETACH(3) Linux Programmer's Manual PTHREAD_DETACH(3)NAME pthread_detach - detach a threadSYNOPSIS #includeint pthread_detach(pthread_t thread); Compile and link with -pthread.
通过这个接口,分离出去的线程依然属于进程内部,但不需要被等待了。举个例子,之前再讲线程与进程的关系时,我们把不同的线程比作家庭成员,做好自己分内的事情,既可以让家庭幸福,即进程成功运行。而进程分离就好比你长大了,自己搬出去住,不受父母管了,但是依旧属于这个家庭。这种状态就是线程分离。
当然,如果想要将自己分离出去,就要知道自己的tid,这里需要接口:
代码语言:javascript代码运行次数:0运行复制PTHREAD_SELF(3) Linux Programmer's Manual PTHREAD_SELF(3)NAME pthread_self - obtain ID of the calling threadSYNOPSIS #includepthread_t pthread_self(void); Compile and link with -pthread.
这个接口会返回调用它的线程的ID。如同getpid()
代码语言:javascript代码运行次数:0运行复制void *ThreadRun(void *args){ // 线程分离 pthread_detach(pthread_self()); std::string name = static_cast(args); while (true) { std::cout << name << "is running ..." << std::endl; sleep(3); break; } return args;}std::string ToHex(int x){ char buffer[128]; snprintf(buffer, sizeof(buffer), "0x%x", x); return buffer;}const int num = 10;int main(){ std::vector tids; for (int i = 0; i < num; i++) { // 1. 线程ID pthread_t tid; // 2. 线程名字 char *name = new char[128]; snprintf(name, 128, "thread - %d", i + 1); pthread_create(&tid, nullptr, ThreadRun, name); // 保存所有线程的ID tids.push_back(tid); } sleep(3); for (auto tid : tids) { pthread_cancel(tid); std::cout << " cancel: " << ToHex(tid) << std::endl; void *ret = nullptr; int n = pthread_join(tid, &ret); std::cout << (long long int)ret << "
quit... , n: " << n << std::endl; } return 0;} 可以看到,如果我们等待一个已经分离出去的线程,会得到22号错误信息!所以不能 join 一个分离的线程!
所以主线程就可以不管新线程,可以继续做自己的事情,不用阻塞在join!
但是注意:线程分离了,依然是同一个进程!一个线程出异常,会导致整个进程退出!
上面是自己分离自己。也可以通过主线程分离新进程:
代码语言:javascript代码运行次数:0运行复制 for (auto tid : tids) { pthread_detach(tid);//主线程分离新线程 }4 语言层的线程封装上面讲的是Linux系统提供给我们的系统调用,帮助我们可以进行线程控制,也叫做原生线程库。我们熟悉了底层的原生线程库,就会方便很多。 我们来看C++11中的线程
代码语言:javascript代码运行次数:0运行复制#include#include #include #include #include #include #include void threadrun( int num){ while (num) { std::cout << " num: " << num << std::endl; }}// C++中线程库int main(){ std::thread mythread(threadrun, 10); while (true) { std::cout << "main thread..." << std::endl; sleep(1); } mythread.join(); return 0;}
注意,虽然是使用的语言层的线程库,但是依旧要连接thread动态库,因为语言层线程库的本质是对原生线程库接口的封装!!!无论是java还是python都要与原生线程库产生联系
# linux
# 操作系统
# ai
# c++
# Python
# Java
# JavaScript
# define
# 封装
# int
# void
# 指针
# 接口
# 栈
# 堆
# 线程
# 多线程
# 主线程
# Thread
# 对象
# 在这里
# 返回值
# 插入图片
# 自己的
# 多个
# 就会
# 是一个
# 就可以
# 是在
相关栏目:
【
网站优化151355 】
【
网络推广146373 】
【
网络技术251813 】
【
AI营销90571 】
相关推荐:
网站制作公司哪里好做,成都网站制作公司哪家做得比较好,更正规?
如何快速生成橙子建站落地页链接?
微信小程序 闭包写法详细介绍
制作公司内部网站有哪些,内网如何建网站?
HTML5段落标签p和br怎么选_文本排版常用标签对比【解答】
大同网页,大同瑞慈医院官网?
制作企业网站建设方案,怎样建设一个公司网站?
Laravel如何实现邮件验证激活账户_Laravel内置MustVerifyEmail接口配置【步骤】
深圳防火门网站制作公司,深圳中天明防火门怎么编码?
公司门户网站制作公司有哪些,怎样使用wordpress制作一个企业网站?
rsync同步时出现rsync: failed to set times on “xxxx”: Operation not permitted
Laravel Artisan命令怎么自定义_创建自己的Laravel命令行工具完全指南
如何在沈阳梯子盘古建站优化SEO排名与功能模块?
哪家制作企业网站好,开办像阿里巴巴那样的网络公司和网站要怎么做?
软银砸40亿美元收购DigitalBridge 强化AI资料中心布局
香港服务器网站搭建教程-电商部署、配置优化与安全稳定指南
Laravel如何使用缓存系统提升性能_Laravel缓存驱动和应用优化方案
如何在搬瓦工VPS快速搭建网站?
JavaScript如何实现类型判断_typeof和instanceof有什么区别
谷歌浏览器如何更改浏览器主题 Google Chrome主题设置教程
Laravel Blade组件怎么用_Laravel可复用视图组件的创建与使用
在Oracle关闭情况下如何修改spfile的参数
Midjourney怎么调整光影效果_Midjourney光影调整方法【指南】
Laravel如何配置中间件Middleware_Laravel自定义中间件拦截请求与权限校验【步骤】
JavaScript如何实现倒计时_时间函数如何精确控制
怎样使用JSON进行数据交换_它有什么限制
Laravel如何连接多个数据库_Laravel多数据库连接配置与切换教程
原生JS实现图片轮播切换效果
如何用腾讯建站主机快速创建免费网站?
Win11应用商店下载慢怎么办 Win11更改DNS提速下载【修复】
车管所网站制作流程,交警当场开简易程序处罚决定书,在交警网站查询不到怎么办?
Swift开发中switch语句值绑定模式
如何在建站主机中优化服务器配置?
Laravel如何获取当前用户信息_Laravel Auth门面获取用户ID
如何在云主机上快速搭建网站?
百度浏览器如何管理插件 百度浏览器插件管理方法
绝密ChatGPT指令:手把手教你生成HR无法拒绝的求职信
微信h5制作网站有哪些,免费微信H5页面制作工具?
如何快速生成可下载的建站源码工具?
使用spring连接及操作mongodb3.0实例
Laravel如何处理CORS跨域请求?(配置示例)
Laravel API资源类怎么用_Laravel API Resource数据转换
Laravel怎么进行数据库事务处理_Laravel DB Facade事务操作确保数据一致性
米侠浏览器网页图片不显示怎么办 米侠图片加载修复
Python企业级消息系统教程_KafkaRabbitMQ高并发应用
什么是JavaScript解构赋值_解构赋值有哪些实用技巧
详解Android中Activity的四大启动模式实验简述
PHP怎么接收前端传的文件路径_处理文件路径参数接收方法【汇总】
Laravel如何记录自定义日志?(Log频道配置)
香港服务器租用每月最低只需15元?


quit... , n: " << n << std::endl; } return 0;}