Linux -- 线程互斥

一 线程互斥的概念

  大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。

  例如下面我们模拟一个多线程抢票的程序。使用一个全局变量 ticket 表示票的数量,创建多个线程进行抢票,代码如下:

#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<vector>

using namespace std;

#define NUM 5
int ticket = 100;


class threadData
{
public:
	threadData(int number){
		threadname = "thread-" + to_string(number);
	}
				
public:
	string threadname;
};

void* getTicket(void* args)
{
	threadData *td = static_cast<threadData*>(args);		    
	while(1)
	{
		if(ticket > 0)
		{
			usleep(1000);     // 模拟抢票的时间
			 printf("%s is running, get a ticket: %d\n", td->threadname.c_str(), ticket);
			ticket--;
			}
		else{
         break;
        }
	}
	return nullptr;
}


int main(){
	 vector<pthread_t> tids;
	 vector<threadData*> thread_datas;

	 for(int i = 1; i <= NUM; i++){
	    threadData* td = new threadData(i);
	    pthread_t tid;
		pthread_create(&tid, nullptr, getTicket, td);
		tids.push_back(tid);
		thread_datas.push_back(td);
	}
				    
	for(auto e : tids){
      pthread_join(e, nullptr);
    }		

	for(auto td : thread_datas){
	  delete td;
    }

	return 0;
}

我们运行起来之后,会看到线程抢到了负数的票! 

  为什么会出现这种情况呢?这种情况我们称为共享数据在无保护的情况下,被多线程并发访问,造成了数据不一致问题!所以对于一个全局变量进行多线程并发减减或者加加,不是安全的!下面我们来分析一下。

  首先需要对 ticket- -,先要将 ticket 读入到 CPU 的寄存器中,然后在 CPU 中要进行计算操作,最后再将 ticket 数据写回内存中。至此就完成了一次 ticket- -,所以上面三个步骤,都会对应每一个汇编语句。

  那么假设我们现在有两个线程,分别为线程1和线程2,在线程执行的代码间隙中,线程是随时有可能会被切换的!而线程在执行的时候,将共享数据加载到 CPU 寄存器的本质就是把数据的内容变成了自己上下文的内容!也就是以拷贝的方式给自己单独拿了一份!

  那么如果线程1刚好读取到内存中的数据,假设此时数据还是100,此时它要被切换了,那么它就要把自己上下文数据保存起来,而保存上下文的本质就是以拷贝的方式,给自己单独拿了一份!那么此时线程1就把100保存到自己的上下文中了。

   

  接下来线程2就开始抢票了,此时线程2在它的时间片内已经抢了99张票了!此时内存中只剩下一张票!

  那么当线程2切换后,线程1继续拿着它的上下文数据放回CPU中计算,注意,此时线程1中的 ticket 还是100,那么计算完后为99,再将 99 写回内存中!此时就导致了 ticket 的数据不一致问题!所以 ticket- - 操作是不安全的!也就是它不具备原子性!

  

  另外,我们不仅仅在对 ticket- -,这种叫做数值计算,而且还在对 ticket 做判断是否大于0,这个过程也是在对 ticket 计算,这种叫做逻辑运算!所以,假设当前 ticket 为1了,在判断期间,可能会有多个线程在进行判断!因为一个线程在判断的期间有可能会被切走!此时它们每一个线程的上下文中都认为 ticket 是1,所以会将 ticket 减到负数!而且判断完毕之后,ticket 就不会被用了,在计算 ticket- - 的时候要重新到内存中读取数据!

  那么这个问题要怎么解决呢?对于共享数据的访问,需要保证任何时候只有一个执行流访问,这就是互斥!所以我们需要通过互斥的方式来解决,也就是互斥锁!接下来我们就开始学习互斥锁。

二 互斥锁 

   在 Linux 中,pthread 库给我们提供了一种互斥锁解决上面多线程访问共享数据不一致的问题。

互斥锁,顾名思义,我们用一个锁将多个线程访问的代码锁住,使其只有一个线程有钥匙,也就是同时只让一个线程进行访问,进而实现了互斥。

接下来我i们认识一下互斥锁的相关接口:

2.1 锁的初始化

  pthread_mutex_init()

#include <pthread.h>


int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);

  该接口是对一把锁初始化。我们可以看到第一个参数的类型是 pthread_mutex_t,这是库给我们提供的一种数据类型,也可以理解为锁的类型!

  第一个参数是输入型参数,我们定义一个 pthread_mutex_t 类型的锁传入它的地址即可

  第二个参数代表这把锁的属性,我们也不管,设置为 nullptr 即可。

  其实,初始化一把锁有两种方式,以上是一种方式,下面还有一种方式是定义一把全局的锁,如果我们使用下面的方法定义了一把锁,就不需要使用上面的方式了;而且也不用释放这把锁了,但是释放也没有问题。其中定义全局锁是固定的,如下:

				pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

2.2 锁的释放

pthread_mutex_destroy()

 #include <pthread.h>

 int pthread_mutex_destroy(pthread_mutex_t *mutex);

  该接口是释放一把锁。第一个参数和初始化时的第一个参数一样。注意如果我们使用 pthread_mutex_init() 的方式初始化一把锁,必须要使用 pthread_mutex_destroy() 进行释放;但是使用全局锁就可以不用释放。

2.3 加锁

  pthread_mutex_lock()

 #include <pthread.h>

 int pthread_mutex_lock(pthread_mutex_t *mutex);

该接口就是对一把锁进行加锁(也就是对该锁进行使用,上锁)

2.4 解锁

 #include <pthread.h>

int pthread_mutex_unlock(pthread_mutex_t *mutex);

该接口就是对一把锁进行解锁,也就是解除对资源的锁定。

2.5 使用示例与说明

  下面我们就使用上面抢票的代码进行加锁,对 ticket 加锁。根据我们以前的知识,这个 ticket 就变成了临界资源,而临界资源并不是全部代码都在访问,而是只有一小部分在访问,我们就把这一小部分的代码的区域称为临界区。

  其实加锁的本质是用时间来换取安全,加锁的表现就是线程对于临界区代码需要串行执行,也就是类似于排队,所以加锁的原则就是尽量要保证临界区代码越少越好!所以上述的代码中,临界区应该是对 ticket 进行访问的区域!如下代码:

首先我们在类中定义一把锁,方便每个线程都有自己的锁:

class threadLock
{//我们将锁的id和锁姓名封装成一个类
public:
    std::string _name; // 给程序员识别的
    pthread_mutex_t* _lock; //让不同的线程使用的锁
public:
    threadLock(int num,pthread_mutex_t* Lock)
    :_lock(Lock)
    {
        _name="Thread"+std::to_string(num);
    }
};

  接下来我们在主函数中定义一把锁,注意,这里定义的锁,是在 main() 函数的栈帧中的,也就是主线程中的,由于我们抢票的程序也在主函数中,所以这样定义不会有问题;最后在主函数返回前释放锁,代码如下:

 

int main(){

    pthread_mutex_t lock; //定义一把锁
    pthread_mutex_init(&lock,nullptr); //进行锁的初始化

	std::vector<pthread_t> tids; //线程编号数组 这两组是为了方便释放
	std::vector<threadLock*> thread_locks; //锁编号数组
    
    for(int i=0;i<=NUM;i++){ //循环创建线程,进行抢票
        
        threadLock* td=new threadLock(i,&lock); //创建一个新锁
        pthread_t tid; 
        pthread_create(&tid, nullptr, getTicket, td);//创建线程 同时抢票
        //让线程和锁进入不同的数组
        tids.push_back(tid);
		thread_locks.push_back(td);
    }
     
     //循环释放
     for(auto e : tids){
      pthread_join(e, nullptr);
     }
				
				
	for(auto td : thread_locks){
        delete td;
     }
				       
    //删除锁
    pthread_mutex_destroy(&lock);
	return 0;
}

接下来是抢票的程序:

void* getTicket(void* args)
{
    //获取锁名和id
	threadLock *td = static_cast<threadLock*>(args);		    
	while(1){ //循环抢票
        //上锁 每一次我们将要访问下面这段代码时,除了当前线程,禁止其它线程访问
        pthread_mutex_lock(td->_lock);
		if(ticket > 0){//有票 
			usleep(1000);     // 模拟抢票的时间
			 printf("%s is running, get a ticket: %d\n", td->_name.c_str(), ticket);
			ticket--;
            pthread_mutex_unlock(td->_lock); //关锁
            usleep(10);
		}
		else{ //无票 关锁 退出
           pthread_mutex_unlock(td->_lock);
           break;
        }

        //pthread_mutex_unlock(td->_lock); //在这里不如上面分两端写,两端写锁占用的资源更少
	}
    std::cout << td->_name.c_str() << "` quit!" << std::endl;
	return nullptr;
}

头文件及杂项:

#include<iostream>
#include<pthread.h>
#include<string>
#include<vector>
#include<time.h>
#include<unistd.h>


const int NUM=5; //创建的线程数量
int ticket=10000; //票数

 

  如上代码,在加锁和解锁锁的区域中,就称为临界区。其中,执行流在申请锁的时候,如果申请锁成功,才能往后执行后面的代码,如果不成功,就会阻塞等待!

  执行的结果如下:

我们可以看到,ticket 不会出现 0 和负数的情况了,也就是说,临界资源被并发访问导致数据不一致问题已经解决了!

但是代码中有些细节我们还需要讲解一下。

  1. 在抢票的程序中,我们可以看到,在一个线程抢完票后,解锁后,我们在其后面加了一句 usleep(10);,这是什么意思呢?很简单,当一个线程加锁后,其它线程就被阻塞等待挂起了,那么当该线程解锁时,其它线程还没来得及从阻塞状态转为运行状态,该线程又去申请锁了,也就是说,唤醒线程的成本更大;而且,我们抢完票后还有后续的代码需要执行,比如处理票的后续动作,这里我们就没有实现。也就是说,所以我们在一个线程解锁后,加上短暂的休眠时间,一是为了有时间唤醒其它线程,二是为了模拟抢票后的后续动作。
  2. 如果我们没有加上 usleep(10); 这句代码,那么该线程就会一直占用这把锁,所以导致票就被它抢完了。那么也就是说,这种纯互斥环境,如果锁分配不够合理,容易导致其它线程一直没有等待到资源,进程一直等待,一直在消耗,直到饿死,这就是饥饿问题。但是不是说只要有互斥,必有饥饿,而是适合纯互斥的场景,就用互斥!
  3. 新来的线程,必须要从等待队列的最后开始排队;解锁的线程,不能马上重新申请锁,必须也要从等待队列的最后开始排队。这就可以让所有的线程获取钥匙,按照一定的顺序,这种按照一定顺序性获取资源的称为同步,这个我们后面详谈。
  4. 每一个线程进入临界区访问临界资源的时候,首先需要申请加锁,所以锁本身就是共享资源,也就是临界资源!所以申请加锁和解锁本身就被设计为原子性的操作了!如何做到的呢?我们后面讲原理再谈。
  5. 那么在临界区中,线程可以被切换吗?可以切换!因为在线程被切出去的时候,是持有锁被切走的,所以在该线程被切换的时候,其他线程也不能进临界区访问临界资源,因为锁只有一把!所以对于其它线程来说,一个线程要么没有锁,要么释放锁,当前线程访问临界区的过程,对于其它线程是原子的!

三 锁的原理

  我们已经知道,ticket>0 不是原子的,因为这个操作会被分为三个汇编语句,那么什么是原子的呢?在计算机底层,我们认为,一条汇编语句就是原子的!

  为了实现互斥锁操作,大多数体系结构都提供了 swapexchange 指令,该指令的作用是把寄存器和内存单元的数据交换,由于只有一条汇编指令,保证了原子性。现在我们把 lockunlock 的伪代码演示一下:

  首先 movb 就是把 0 写入 al 寄存器,可以理解为 eax 寄存器:

  接下来 xchgb 就是将 al 寄存器中的内容和内存中定义的一个变量 mutex 进行交换,其中 mutex 的初始值为1:

接下来对 al 寄存器中的值进行判断,如果大于 0,说明申请加锁成功,否则申请加锁失败,挂起等待。

上面我们演示的都是一个线程来申请加锁,如果有两个线程来申请加锁呢?例如,线程1和线程2来申请加锁,而加锁的语句是一句,但是它被分为上面多个汇编语句,所以当一个线程执行到某一个汇编语句的时候,随时都有可能被切换!

假设线程1申请加锁的过程中,刚刚执行完第一步,即将 0 写入了 al 寄存器中,实际上是写入线程的硬件的上下文中。此时线程2来了,线程1要被切走,所以线程1将 al 寄存器中的内容保存起来,即将 0 保存起来,当切换回来的时候执行 xchgb 语句。  

线程2来的时候,再次将0写入 al 寄存器中,然后执行xchgb语句,将 al 寄存器中的内容和内存的内容交换,交换完成后,al 寄存器中的内容变成1,线程2中的上下文内容也变成1,正常来说线程2此时做判断,此时al寄存器的值大于0,所以可以直接返回。

但是如果在线程2做判断的时候,线程2需要被切走,线程1切回来,首先先要将上下文恢复回来,此时将 al 寄存器中的内容恢复成为0,然后和内存中的值交换,交换完后发现 al 寄存器中的值为 0,此时线程1就被挂起等待了。 

线程1被挂起等待后不会被调度,所以此时线程2被切回来,恢复上下文,把1放回al寄存器中,然后做判断,大于0,申请加锁成功,返回0。

所以从上面的过程我们可以看出,其实 xchgb 的语句最重要。交换的本质就是把内存中的数据,交换到CPU的寄存器中,也就是将数据交换到线程的上下文中!而线程上下文是线程私有的!另外,内存中的数据是被所有线程共享的,而锁只有一把,所以申请加锁的本质就是把一把共享的锁,让一个线程以一条汇编的方式,交换到自己的上下文中,就代表当前线程持有锁了!

那么解锁的汇编语句如下:

 

  其实就是将内存中的数据重置为1即可。并没有将上一次线程申请加锁的 1 交换回内存中,因为并不需要,因为每一个线程在申请加锁的时候首先需要将 0 写入 al 寄存器中,也就是写入自己的硬件上下文中,此时就相当于将原来申请过加锁的 1 覆盖掉。 

四、可重入和线程安全

4.1 概念

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题;
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

也就是说,如果一个函数不可重入,那么在多线程执行时,可能会出现线程安全问题。如果一个函数可被重入的,那么就一定不会出现线程安全问题。

4.2 可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的;
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题,如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

4.3 可重入与线程安全区别

  • 可重入函数是线程安全函数的一种;
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的;
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

最后总结就四个结论:

  • 线程安全描述的是并发的问题
  • 可重入描述的是函数特点的问题
  • 不可重入函数在多线程访问时,可能会出现线程安全问题
  • 可重入函数在多线程访问时,不会有线程安全问题

五、死锁

5.1.死锁概念

死锁是指在一组执行流中的一个线程持有一把锁,另一个线程持有另一把锁,但因互相申请对方的锁,并不释放自己的锁而处于的一种永久等待状态。

5.2 死锁的必要条件

首先我们了解一下什么叫做死锁的必要条件,也就是只要产生了死锁,必定所有的条件都要满足。也就是以下四个条件都要满足:

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

5. 3. 避免死锁

  • 破坏死锁的四个必要条件之一
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

激光打标技术:现代制造业的精准标记解决方案

随着科技的飞速进步&#xff0c;激光打标机技术已经成为现代制造业中不可或缺的一部分。作为一种快速、精确、耐用的标记解决方案&#xff0c;激光打标技术以其独特的优势&#xff0c;为现代制造业提供了精准、高效、持久的标记解决方案。 首先&#xff0c;激光打标技术以其无与…

吴恩达机器学习笔记 十七 通过偏差与方差诊断性能 正则化 偏差 方差

高偏差&#xff08;欠拟合&#xff09;&#xff1a;在训练集上表现得也不好 高方差&#xff08;过拟合&#xff09;&#xff1a;J_cv要远大于J_train 刚刚好&#xff1a;J_cv和J_train都小 J_cv和J_train与拟合多项式阶数的关系 从一阶到四阶&#xff0c;训练集的误差越来越小…

挂耳式耳机什么牌子的好?掌握六大挂耳式耳机选购秘诀

随着科技的进步&#xff0c;蓝牙耳机逐渐成为人们日常生活中的热门配件。很多人选择蓝牙耳机&#xff0c;是为了在娱乐学习时享受便捷的无线体验。这些耳机不仅设计时尚&#xff0c;佩戴起来也极为舒适。 蓝牙耳机主要分为挂耳式和入耳式。尽管入耳式耳机功能齐全&#xff0c;…

在集群模式下,Redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?

目录 一、分布式寻址算法 1. hash 算法 2. 一致性 hash 算法 3. Redis cluster 的 hash slot 算法 二、Redis cluster 的高可用与主备切换原理 1. 判断节点宕机 2. 从节点过滤 3. 从节点选举 4. 与哨兵比较 一、分布式寻址算法 hash 算法(大量缓存重建) 一致性 hash…

Python的time模块与datetime模块大揭秘!

1.time 模块 t主要用来操作时间&#xff0c;还可以用于控制程序 导入time模块 import time 2.获取从1970年1月1日0时0分0秒距今的秒数&#xff1a;time.time() print(time.time()) 3.格式化显示时间&#xff1a;time.strftime() print(time.strftime("%Y-%m-%d %H:…

某赛通电子文档安全管理系统 DecryptApplication 任意文件读取漏洞复现

0x01 产品简介 某赛通电子文档安全管理系统(简称:CDG)是一款电子文档安全加密软件,该系统利用驱动层透明加密技术,通过对电子文档的加密保护,防止内部员工泄密和外部人员非法窃取企业核心重要数据资产,对电子文档进行全生命周期防护,系统具有透明加密、主动加密、智能…

使用 Python+Selenium + 第三方库实现简单的web自动化测试框架 源码

一、配置(config) 1.1 说明 设置自动化案例运行时的属性值。 安排自动化案例的执行顺序。 所在路径&#xff1a; …\Project_Selenium\config 1.2 文件 1.2.1 config.ini 目录&#xff1a; …\Project_Selenium\config\config.ini 配置字段&#xff1a; 1 [PROJECT] 1.1 bro…

【C++那些事儿】深入理解C++类与对象:从概念到实践(下)| 再谈构造函数(初始化列表)| explicit关键字 | static成员 | 友元

&#x1f4f7; 江池俊&#xff1a;个人主页 &#x1f525; 个人专栏&#xff1a;✅C那些事儿 ✅Linux技术宝典 &#x1f305; 此去关山万里&#xff0c;定不负云起之望 文章目录 1. 再谈构造函数1.1 构造函数体赋值1.2 初始化列表1.3 explicit 关键字 2. static成员2.1 概念…

十四、Nacos源码系列:Nacos配置发布原理

目录 一、简介 二、加密处理 三、发布配置 3.1、插入或更新配置信息 3.2、发布配置数据变动事件 3.2.1、目标节点是当前节点 3.2.2、目标节点非当前节点 四、总结 一、简介 一般情况下&#xff0c;我们是通过Nacos提供的Web控制台登录&#xff0c;然后通过界面新增配置…

个人博客系列-后端项目-用户注册功能(7)

介绍 用户注册API的主要流程&#xff1a;1.前端用户提交用户名&#xff0c;密码 2. 序列化器校验用户名&#xff0c;密码是否合法。3.存入数据库。4.签发token 创建序列化器 from rest_framework import serializers from rest_framework_simplejwt.serializers import Toke…

图【数据结构】

文章目录 图的基本概念邻接矩阵邻接表图的遍历BFSDFS 图的基本概念 图是由顶点集合及顶点间的关系组成的一种数据结构 顶点和边&#xff1a;图中结点称为顶点 权值:边附带的数据信息 路径 &#xff1a; 简单路径 和 回路&#xff1a; 子图&#xff1a;设图G {V, E}和图G1…

计算机网络:关键性能指标与非性能特征解析

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

VR文化旅游虚拟现实介绍|虚拟现实元宇宙|VR设备购买

虚拟现实&#xff08;VR&#xff09;技术正在改变我们对文化旅游的认知和体验。通过VR技术&#xff0c;人们可以身临其境地探索世界各地的文化遗产和旅游景点&#xff0c;无需亲临现场也能感受到逼真的体验。以下是VR文化旅游虚拟现实的介绍&#xff1a; 身临其境的体验&#x…

c++之旅——第六弹

大家好啊&#xff0c;这里是c之旅第六弹&#xff0c;跟随我的步伐来开始这一篇的学习吧&#xff01; 如果有知识性错误&#xff0c;欢迎各位指正&#xff01;&#xff01;一起加油&#xff01;&#xff01; 创作不易&#xff0c;希望大家多多支持哦&#xff01; 一,静态成员&…

安装Mysql和Mycli插件

一、安装数据库 1.重定向生成配置文件 cat >/etc/yum.repos.d/mysql.repo <<EOF [mysql57-community] nameMySQL 5.7 Community Server baseurlhttp://repo.mysql.com/yum/mysql-5.7-community/el/7/x86_64/ enabled1 gpgcheck0 EOF 2.yum安装 yum -y install mysq…

eclipse导入项目出现中文乱码

eclipse导入java项目的时候有时会出现乱码问题&#xff0c;很苦恼&#xff0c;网上找了很多方法都没用&#xff0c;所以得自己记录一下。导入项目可参考链接 eclipse中导入java项目-CSDN博客 1、点击 Windows --> Pereferences 2、依次点击下图内流程 3、看到下面的就修改成…

基于YOLOv8/YOLOv7/YOLOv6/YOLOv5的血细胞智能检测与计数(深度学习模型+UI界面代码+训练数据集)

摘要&#xff1a;开发血细胞智能检测与计数系统对于疾病的预防、诊断和治疗具有关键作用。本篇博客详细介绍了如何运用深度学习构建一个血细胞智能检测与计数系统&#xff0c;并提供了完整的实现代码。该系统基于强大的YOLOv8算法&#xff0c;并对比了YOLOv7、YOLOv6、YOLOv5&a…

弧形导轨的设计要求

制造业设备种类越来越多&#xff0c;非标自动化设备渐渐成了主力市场&#xff0c;其中弧形导轨线体作为非标自动化运输中的基石&#xff0c;承担了运输&#xff0c;定位&#xff0c;特殊工位组装&#xff0c;其设计要求也非常严格。 1、精度要求&#xff1a;弧形导轨需要具备高…

【C++】开源:iceoryx通信中间件配置与使用

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍iceoryx通信中间件配置与使用。 学其所用&#xff0c;用其所学。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c;下次更新…

Linux运维之管理工具篇

一、前言 因运维过程中&#xff0c;经常会借助于很多工具来实现我们的监控、备份、校验&#xff0c;安全测试&#xff0c;批量操作&#xff0c;可视化辅助&#xff0c;集中管理等&#xff0c;甚至AI相关&#xff0c;本文特对常用工具进行梳理记录&#xff0c;以备不时之需及后…