Linux操作系统——多线程

1.线程特性

1.1线程优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

1.2线程缺点

  • 性能损失
    •  一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  • 健壮性降低
    • 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
  • 缺乏访问控制
    • 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  • 编程难度提高
    • 编写与调试一个多线程程序比单线程程序困难得多。

1.3线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

1.4线程用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

2.编写代码理解多线程

首先我们创建一个Makefile编写如下代码:

mythread:mythread.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -rf mythread

然后创建一个mythread.cc的c++源文件编写如下代码:

#include<iostream>
#include<pthread.h>
#include<unistd.h>void * ThreadRoutine(void * arg)
{while(true){std::cout<<"I am a new pthread"<<std::endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,ThreadRoutine,nullptr);while(true){std::cout<<"I am a main pthread"<<std::endl;sleep(1);}return 0;
}

然后编译运行发现报错了:

g++说不认识这个pthread_create这个接口,那么我们就需要来谈一个话题了,Linux有没有真正的线程呢?没有,内核中只有轻量级进程的概念,所以Linux操作系统只会提供轻量级进程创建的系统调用,不会直接提供线程创建的接口。

因为可能用户学习的操作系统中是有线程这个概念的,但是Linux内核只认轻量级进程LWP所以两者就无法达成共识,为了让用户认为自己创建的是线程,然后Linux操作系统认为创建的是轻量级进程,所以就有了中间的软件层,pthread原生线程库,这个库一般都是跟linux配套在一起的,所以不用担心用户因为没有这个pthread原生线程库而调用创建线程的接口而失败。但是又有人说了,为什么Linux非得用轻量级进程而不去实现线程,因为轻量级进程又不得不实现一个像pthread原生线程库这样的库,这不是多此一举吗?其实并不是这样的,用轻量级进程LWP模拟线程本就是Linux操作系统的一大亮点,而且中间有一层pthread原生线程库反而可以让接口层与实现层进行解耦,未来如果pthread原生线程库要是更新了也不会影响到Linux内核,比如说无论上层怎么更新同样可以是用同一版本的Linux内核,这样维护性的效率就大大提高了,这样的实现更符合软件工程。这个库我们是可以通过搜索该路径看到的:

所以上述代码报错的原因是找不到我们对应的库,所以我们将Makefile中的代码改为:

mythread:mythread.ccg++ -o $@ $^ -std=c++11 -lpthread.PHONY:clean
clean:rm -rf mythread

运行结果:

我们可以查看一下链接的库:

此时两个线程就开始运行了。

下面我们对线程创建再进行扩展:

比如说我们如何给线程传参呢?如何传递线程创建的时间啊,执行的任务,线程名称

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<functional>
#include<time.h>using func_t = std::function<void()>;class ThreadData
{
public:ThreadData(const std::string name,const uint64_t ctime,func_t f):threadname(name),createtime(ctime),func(f){}
public:std::string threadname;uint64_t createtime;func_t func;
};void Print()
{std::cout<<"我是线程执行的大任务的一部分"<<std::endl;
}void * ThreadRoutine(void * args)
{ThreadData *td = static_cast<ThreadData*>(args);while(true){std::cout<<"I am a new pthread"<<"threadname: "<<td->threadname<<"create time: "<<td->createtime<<std::endl;td->func();sleep(1);}
}int main()
{pthread_t tid;ThreadData * td = new ThreadData("thread 1",(uint64_t)time(nullptr),Print);pthread_create(&tid,nullptr,ThreadRoutine,td);while(true){std::cout<<"I am a main pthread"<<std::endl;sleep(1);}return 0;
}

运行结果:

下面如何修改代码变成创建多线程呢?

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<functional>
#include<time.h>
#include<vector>const int threadnum = 5;
using func_t = std::function<void()>;class ThreadData
{
public:ThreadData(const std::string name,const uint64_t ctime,func_t f):threadname(name),createtime(ctime),func(f){}
public:std::string threadname;uint64_t createtime;func_t func;
};void Print()
{std::cout<<"我是线程执行的大任务的一部分"<<std::endl;
}void * ThreadRoutine(void * args)
{ThreadData *td = static_cast<ThreadData*>(args);while(true){std::cout<<"I am a new pthread"<<"threadname: "<<td->threadname<<"create time: "<<td->createtime<<std::endl;td->func();sleep(1);}
}int main()
{std::vector<pthread_t> pthreads;for(int i = 0;i<threadnum;i++){pthread_t tid;char threadname[64];snprintf(threadname,sizeof(threadname),"%s - %lu","thread",i+1);ThreadData * td = new ThreadData(threadname,(uint64_t)time(nullptr),Print);pthread_create(&tid,nullptr,ThreadRoutine,td);pthreads.push_back(tid);sleep(1);}while(true){std::cout<<"I am a main pthread"<<std::endl;sleep(1);}return 0;
}

把创建线程的代码放入for循环,然后将threaname都有不同的线程名称,而且将tid保存起来我们用到了vector.

下面我们运行一下:

所以这里我们就创建5个新线程。

下面我们来研究两个问题:

1.线程的健壮性问题:当一个进程有多个线程时,只要有一个线程触发了异常,整个进程也会受到相应的影响。

比如说我们修改一个函数中的代码故意制造除零错误触发段错误来进行验证:

void * ThreadRoutine(void * args)
{int a = 10;ThreadData *td = static_cast<ThreadData*>(args);while(true){std::cout<<"I am a new pthread"<<"threadname: "<<td->threadname<<"create time: "<<td->createtime<<std::endl;td->func();if(td->threadname=="thread - 4"){std::cout<<td->threadname<<" 触发了异常"<<std::endl;a/=0;}sleep(1);}
}

当线程名称为第四个的时候让其触发段错误,促使让操作系统发送8号信号让进程终止。

我们通过监视窗口可以看到,进程直接被终止了,说明一旦线程出现了异常,那么操作系统会给进程发信号让进程退出,那么进程都退了,线程自然也就退了。

2.观察一下thread id

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<functional>
#include<time.h>
#include<vector>const int threadnum = 5;
using func_t = std::function<void()>;class ThreadData
{
public:ThreadData(const std::string name,const uint64_t ctime,func_t f):threadname(name),createtime(ctime),func(f){}
public:std::string threadname;uint64_t createtime;func_t func;
};void Print()
{std::cout<<"我是线程执行的大任务的一部分"<<std::endl;
}void * ThreadRoutine(void * args)
{int a = 10;ThreadData *td = static_cast<ThreadData*>(args);while(true){std::cout<<"I am a new pthread"<<"threadname: "<<td->threadname<<"create time: "<<td->createtime<<std::endl;td->func();// if(td->threadname=="thread - 4")// {//     std::cout<<td->threadname<<" 触发了异常"<<std::endl;//     a/=0;// }sleep(1);}
}int main()
{std::vector<pthread_t> pthreads;for(int i = 0;i<threadnum;i++){pthread_t tid;char threadname[64];snprintf(threadname,sizeof(threadname),"%s - %lu","thread",i+1);ThreadData * td = new ThreadData(threadname,(uint64_t)time(nullptr),Print);pthread_create(&tid,nullptr,ThreadRoutine,td);pthreads.push_back(tid);sleep(1);}for(const auto &tid : pthreads){std::cout<<"thread id : "<<tid<<std::endl;}while(true){std::cout<<"I am a main pthread"<<std::endl;sleep(1);}return 0;
}

在上述代码基础上添加一段打印tid的代码,运行结果:

那么这些thread id这么长的一段数字到底是什么意思呢,为了更清晰的理解这串数字,我们可以将其16进制打印出来。

我们再来认识一个线程获取自身的id的一个接口:

然后再写一段代码:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<functional>
#include<time.h>
#include<vector>std::string toHex(pthread_t tid)
{char id[64];snprintf(id,sizeof(id),"0x%lx",tid);return id;
}void * threadRoutine(void * arg)
{usleep(1000);std::string name = static_cast<const char *>(arg);while(true){std::cout<<"new thread is running, thread name: "<<name<<" thread id : "<<toHex(pthread_self())<<std::endl;sleep(1); }
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,threadRoutine,(void *)"thread-1");while(true){std::cout<<"main thread, sub thread: "<<tid<<" main thread id : "<<toHex(pthread_self())<<std::endl;sleep(1);}return 0;
}

运行结果:

数字有这么长一串是因为我用的是64位系统的,然后这更像是一个地址,其实thread id的本质就是一个地址。

线程既然可以创建那么我们如何把它终止呢?我们继续来认识一个线程终止的接口:

需要通过传一个指针让线程终止,是不是呢?我们通过将新线程运行的函数修改成如下:

void * threadRoutine(void * arg)
{usleep(1000);std::string name = static_cast<const char *>(arg);int cnt = 5;while(cnt--){std::cout<<"new thread is running, thread name: "<<name<<" thread id : "<<toHex(pthread_self())<<std::endl;sleep(1); }//return nullptr;pthread_exit(nullptr);
}

运行结果:

我们发现运行到后面就剩一个主线程了,说明新线程退出了。

下面我们来谈谈关于线程返回值的问题:

1.我们要获取返回值该如何获取呢?

2.线程在本质上其实就是Linux操作系统下的轻量级进程,那么当轻量级进程终止了,它的PCB会不会立即释放呢?

3.线程默认要被等待吗?是的,线程退出,没有等待会导致类似进程的僵尸问题。线程退出时如何获取新线程的返回值呢?

首先我们先认识一个接口:

参数是thread id 和 一个二级指针,只要等待成功了,返回值就是0.

下面我们用这个线程等待的接口来进行测试:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<functional>
#include<time.h>
#include<vector>std::string toHex(pthread_t tid)
{char id[64];snprintf(id,sizeof(id),"0x%lx",tid);return id;
}void * threadRoutine(void * arg)
{usleep(1000);std::string name = static_cast<const char *>(arg);int cnt = 5;while(cnt--){std::cout<<"new thread is running, thread name: "<<name<<" thread id : "<<toHex(pthread_self())<<std::endl;sleep(1); }return nullptr;// pthread_exit(nullptr);
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,threadRoutine,(void *)"thread-1");std::cout<<"main thread,  "<<" main thread id : "<<toHex(pthread_self())<<std::endl;sleep(10);int n = pthread_join(tid,nullptr);std::cout<<"main thread done"<<"n : "<<n<<std::endl;sleep(5);return 0;
}

运行结果:

我们看到最后的返回值是0,所以表示等待成功了。

如果我们要得到新线程的返回值,那么我们得到的也应该是void *,所以为了得到一个void*就需要传入一个void * * 。

那么下面我们来修改一下代码来证明一下:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<functional>
#include<time.h>
#include<vector>std::string toHex(pthread_t tid)
{char id[64];snprintf(id,sizeof(id),"0x%lx",tid);return id;
}void * threadRoutine(void * arg)
{usleep(1000);std::string name = static_cast<const char *>(arg);int cnt = 5;while(cnt--){std::cout<<"new thread is running, thread name: "<<name<<" thread id : "<<toHex(pthread_self())<<std::endl;sleep(1); }return (void*)"thread-1 done";// return nullptr;// pthread_exit(nullptr);
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,threadRoutine,(void *)"thread-1");std::cout<<"main thread,  "<<" main thread id : "<<toHex(pthread_self())<<std::endl;void * ret = nullptr;int n = pthread_join(tid,&ret);std::cout<<"main thread done "<<"n : "<<n<<std::endl;std::cout<<"main get new thread return : "<< (const char *)ret<<std::endl;return 0;
}

运行结果:

果然获取到了对应的返回值。

线程是可以被设置为分离状态的(可以理解为 该线程不受主线程的管控了),线程默认情况下是joinable的.

下面我们用代码来实现线程设置为分离状态,出现的现象也就是主线程等待新线程的返回值不成功:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<functional>
#include<time.h>
#include<vector>std::string toHex(pthread_t tid)
{char id[64];snprintf(id,sizeof(id),"0x%lx",tid);return id;
}void * threadRoutine(void * arg)
{pthread_detach(pthread_self());// usleep(1000);// std::string name = static_cast<const char *>(arg);int cnt = 5;while(cnt--){std::cout<<"thread is running..."<<std::endl;sleep(1); }// return (void*)"thread-1 done";return nullptr;// pthread_exit(nullptr);
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,threadRoutine,(void *)"thread-1");sleep(1);// std::cout<<"main thread,  "<<" main thread id : "<<toHex(pthread_self())<<std::endl;int n = pthread_join(tid,nullptr);std::cout<<"main thread done "<<"n : "<<n<<std::endl;return 0;
}

运行结果:

我们发现返回值n不在是0了,说明等待失败了。

当然也可以这样分离:

int main()
{pthread_t tid;pthread_create(&tid,nullptr,threadRoutine,(void *)"thread-1");sleep(1);pthread_detach(tid);// std::cout<<"main thread,  "<<" main thread id : "<<toHex(pthread_self())<<std::endl;int n = pthread_join(tid,nullptr);std::cout<<"main thread done "<<"n : "<<n<<std::endl;return 0;
}

把pthread_detach()这段代码放到这个位置,用主线程与新线程分离。

如果不分离,运行结果是这样的:

其实我们还有一种可以让线程退出的方式,那就是线程取消掉,用到接口pthread_cancel

下面我们运用这个接口来进行测试:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<functional>
#include<time.h>
#include<vector>std::string toHex(pthread_t tid)
{char id[64];snprintf(id,sizeof(id),"0x%lx",tid);return id;
}void * threadRoutine(void * arg)
{//pthread_detach(pthread_self());// usleep(1000);// std::string name = static_cast<const char *>(arg);int cnt = 5;while(cnt--){std::cout<<"thread is running..."<<std::endl;sleep(1); }// return (void*)"thread-1 done";return nullptr;// pthread_exit(nullptr);
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,threadRoutine,(void *)"thread-1");sleep(5);//pthread_detach(tid);// std::cout<<"main thread,  "<<" main thread id : "<<toHex(pthread_self())<<std::endl;int n = pthread_cancel(tid);std::cout<<"main thread cancel done, "<<"n : "<<n<<std::endl;void * ret = nullptr;n = pthread_join(tid,&ret);std::cout<<"main thread join done,"<<" n : "<<n<<"thread return : "<<(int64_t)ret<<std::endl;return 0;
}

运行结果:

说明我们线程取消成功了,同时被join了,退出码是-1.

我们把线程分离加上去:


int main()
{pthread_t tid;pthread_create(&tid,nullptr,threadRoutine,(void *)"thread-1");sleep(5);pthread_detach(tid);// std::cout<<"main thread,  "<<" main thread id : "<<toHex(pthread_self())<<std::endl;int n = pthread_cancel(tid);std::cout<<"main thread cancel done, "<<"n : "<<n<<std::endl;void * ret = nullptr;n = pthread_join(tid,&ret);std::cout<<"main thread join done,"<<" n : "<<n<<"thread return : "<<(int64_t)ret<<std::endl;return 0;
}

运行结果就是:

线程被分离了也是可以取消的,但是不能被join.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/756246.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《1w实盘and大盘基金预测 day7》

昨日预测有点差劲&#xff0c;最低点也相差五个点。 打分C 公众号&#xff1a;JavaHelmet 昨天预测&#xff1a; 3052-3062-3076-3115 3067是趋势线&#xff0c;有回踩需求 5-30-60分钟级别顶钝 大盘冲到标红的点位3115或者3100就需注意。不要随意追高&#xff08;最高309…

备战蓝桥杯---牛客寒假训练营2VP

题挺好的&#xff0c;收获了许多 1.暴力枚举&#xff08;许多巧妙地处理细节方法&#xff09; n是1--9,于是我们可以直接暴力&#xff0c;对于1注意特判开头0但N&#xff01;1&#xff0c;对于情报4&#xff0c;我们可以把a,b,c,d的所有取值枚举一遍&#xff0c;那么如何判断有…

ModbusTCP转Profinet网关高低字节交换切换

背景&#xff1a;在现场设备与设备通迅之间通常涉及到从一种字节序&#xff08;大端或小端&#xff09;转换到另一种字节序。大端字节序是指高位字节存储在高地址处&#xff0c;而小端字节序是指低位字节存储在低地址处。在不动原有程序而又不想或不能添加程序下可选用ModbusTC…

OCP NVME SSD规范解读-13.Self-test自检要求

4.10节Device Self-test Requirements详细描述了数据中心NVMe SSD自检的要求&#xff0c;这一部分规范了设备自身进行各种健康检查和故障检测的过程。自检对于确保SSD的正常运行和提前预防潜在故障至关重要。 在进行设备自检时&#xff0c;设备应当确保不对用户数据造成破坏&am…

【人工智能】Gitee AI 天数智芯有奖体验开源AI模型,一定能有所收货,快来体验吧

大家好&#xff0c;我是全栈小5&#xff0c;欢迎阅读小5的系列文章。 这是《人工智能》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解。 目录 前言两大赛道天数智芯1.模型地址2.天数智芯专区3.选择模型4.模型详情页5.部署模型6.成功部署7.执行例子8.移除模型 千模盲…

03python注释与输入函数

Python 注释的作用: 注释可用于解释 Python 代码。 注释可用于提高代码的可读性。 在测试代码时,可以使用注释来阻止执行。 注释可以放在一行的末尾,Python 将忽略该行的其余部分: 实例1 print("Hello, World!") #打印输出Hello,World print(9-3) #输出9…

【mybatis】objectwrapper解读

简介 在 MyBatis 中&#xff0c;ObjectWrapper 是一个关键的接口&#xff0c;用于详细封装了对象的属性信息。ObjectWrapper 主要用于内部操作&#xff0c;它抽象了对象的属性操作&#xff0c;使得 MyBatis 能够统一处理原生类型、Bean 对象以及 Map 集合等。 类图展示 主要功…

Vue组件封装方案对比——v-if方式与内置component方式

近期在准备搭建一个通用组件库&#xff0c;而公司现有的各个系统也已有自己的组件库只是没抽离出来&#xff0c;但是目前有两套不同的组件封装方案&#xff0c;所以对于方案的选择比较困惑&#xff0c;于是对两种方式进行了对比&#xff0c;结合网上找到的一些开源组件库进行分…

抖音店铺规划运营管理计划数据分析工作表

【干货资料持续更新&#xff0c;以防走丢】 抖音店铺规划运营管理计划数据分析表 部分资料预览 资料部分是网络整理&#xff0c;仅供学习参考。 抖音小店运营规划工作表格&#xff08;完整资料包含以下内容&#xff09; 目录 1. 抖店运营管理决策表&#xff1a;该表格用于记…

三维指静脉生物识别成像设备设计和多视图验证研究

文章目录 三维指静脉生物识别成像设备设计和多视图验证研究总结摘要介绍多视角指静脉识别模型结构内容特征编码Transformer(CFET)主导特征选择模块(DFSM) 实验和结果数据集实施细节视角研究池化层的作用消融实验和SOTA方法比较 论文: Study of 3D Finger Vein Biometrics on I…

Linux——进程通信(三)命名管道

前言 我们在之前学习了匿名管道与匿名管道的应用——进程池&#xff0c;但是匿名管道的通信&#xff0c;需要有血缘关系的进程&#xff08;通过fork创建的进程们&#xff09;&#xff0c;如果我想让两个毫不相干的进程进行通信&#xff0c;可以采样命名管道的方式&#xff08;…

Go语言中的锁与管道的运用

目录 1.前言 2.锁解决方案 3.管道解决方案 4.总结 1.前言 在写H5小游戏的时候&#xff0c;由于需要对多个WebSocket连接进行增、删、查的管理和对已经建立连接的WebSocket通过服务端进行游戏数据交换的需求。于是定义了一个全局的map集合进行连接的管理&#xff0c;让所有…

80后深圳设计师原创设计 妙解中小学生午休难题

3月17日至21日&#xff0c;深圳国际智能家居博览会在宝安国际会展中心举办。智慧校园展区成为焦点&#xff0c;吸引了众多目光。智荟康科技展出的午休课桌椅产品&#xff0c;为解决中小学生“趴睡”问题而研发&#xff0c;创新实用&#xff0c;在智慧校园展区中备受好评。 &…

Leetcode 79. 单词搜索

心路历程&#xff1a; 做完这道题才发现是回溯&#xff0c;一开始想的是递归&#xff0c;判断完第i个字符后&#xff0c;只需要挨个判断第i1个字符在不在第i个字符的邻域。后来发现由于不能重复使用元素&#xff0c;所以需要维护一个visited列表&#xff0c;并且在遍历所有可能…

蓝桥杯---代分数

import java.util.Scanner;public class top4 {//全排列分数的那个题目//首先进行n个数的全排列//然后将这n个数字拆分为3个数字&#xff0c;即插入两个板子//然后判断等式是否成立&#xff08;判断条件就是在if里面去进行相关的判断是吗&#xff1f;&#xff1f;&#xff09;s…

Spring项目部署到linux上

目录 一、环境配置 1、数据准备 2、程序配置文件修改 二、打包项目 三、上传jar包到服务器 四、开放端口号 五、运行程序 六、查看日志 七、常见问题 1、服务未启动 2、端口号已被占用 3、端口未开放 一、环境配置 1、数据准备 需要先在linux环境下的数据库中创建…

Go语言之函数、方法、接口

一、函数 函数的基本语法&#xff1a; func 函数名&#xff08;形参列表&#xff09;&#xff08;返回值列表&#xff09; {执行语句...return 返回值列表 } 1.形参列表&#xff1a;表示函数的输入 2.函数中的语句&#xff1a;表示为了实现某一功能的代码块 3.函数可以有返回…

【Linux】进程排队的理解进程状态的表述僵尸进程和孤儿进程的理解

一、进程排队的理解 进程不是一直运行的&#xff0c;进程可能会在等待某种软硬件资源。即使把进程加载到CPU中&#xff0c;也不是一直会运行的。而进程排队&#xff0c;一定是在等待某种软硬件资源&#xff08;可以是CPU&#xff0c;键盘&#xff0c;磁盘&#xff0c;网卡等等设…

rabbitmq-spring-boot-start配置使用手册

rabbitmq-spring-boot-start配置使用手册 文章目录 1.yaml配置如下2.引入pom依赖如下2.1 引入项目resources下libs中的jar包依赖如下2.2引入maven私服依赖如下 3.启动类配置如下4.项目中测试发送消息如下5.项目中消费消息代码示例6.mq管理后台交换机队列创建及路由绑定关系如下…

深入探讨Python中的文件操作与文件IO操作【第141篇—Python实现】

&#x1f47d;发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 深入探讨Python中的文件操作与文件IO操作 在Python编程中&#xff0c;文件操作和文件IO操作…