当前位置: 首页 > news >正文

【Linux】线程互斥 -- 互斥锁 | 死锁 | 线程安全

  • 引入
  • 互斥
    • 初识锁
    • 互斥量mutex
    • 锁原理解析
  • 可重入VS线程安全
    • STL中的容器是否是线程安全的?
  • 死锁

引入

我们写一个多线程同时访问一个全局变量的情况(抢票系统),看看会出什么bug:

// 共享资源, 火车票
int tickets = 10000;
//新线程执行方法
void *getTicket(void *args)
{std::string username = static_cast<const char *>(args);while (true){if (tickets > 0){usleep(1254); // 1秒 = 1000毫秒 = 1000 000 微妙 = 10^9纳秒std::cout << username << " 正在进行抢票: " << tickets << std::endl;// 用这段时间来模拟真实的抢票要花费的时间tickets--;}else{break;}}return nullptr;
}
//主线程
//跟之前一样创建多个线程然后调用这个getTicket方法就行

假如创建4个线程同时抢票,总票数有10000张,每个线程抢到票以后减一,按照正常情况我们应该是抢票到0截至。

多个线程交叉执行本质:就是让调度器尽可能的频繁发生线程调度与切换
线程一般在什么时候发生切换呢?时间片到了,来了更高优先级的线程,线程等待的时候。
线程是在什么时候检测上面的问题呢?从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以,就直接发生线程切换

在这里插入图片描述
实验结果:假如此时tickets = 1,第1号线程先判断了if (tickets > 0)然后进入语句中,结果被usleep阻塞了,这时候切换了另外的线程,此时tickets的值还未被1号进程更改,所以它同样也能进入语句中,就这样导致tickets--被执行了多次,然后就出现上述的负数结果。

对变量进行++,或者–,在C、C++上,看起来只有一条语句,但是汇编之后至少是三条语句:

  1. 从内存读取数据到CPU寄存器中
  2. 在寄存器中让CPU进行对应的算逻运算
  3. 写回新的结果到内存中变量的位置

模拟一下线程1与线程2的更改切换逻辑:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
我们定义的全局变量,在没有保护的时候,往往是不安全的,像上面多个线程在交替执行造成的数据安全问题,发生了数据不一致问题!


互斥

初识锁

  • 临界资源:多个执行流进行安全访问的共享资源
  • 临界区:我们把多个执行流中,访问临界资源的代码(往往是线程代码的很小的一部分)
  • 互斥:想让多个线程串行访问共享资源,任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  • 原子性:对一个资源进行访问的时候,要么不做,要么做完,该操作只有两态,不会被任何调度机制打断的操作。(只用一条汇编语句就能执行完的算是原子,当然还有别的情况之后再详细说明)

在这里插入图片描述

在这里插入图片描述
初步使用加锁解锁:

//定义为全局的锁,无需初始化和销毁,直接以这样的形式使用:
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *getTicket(void *args)
{std::string username = static_cast<const char *>(args);while (true){pthread_mutex_lock(&lock);if (tickets > 0){usleep(1254); // 1秒 = 1000毫秒 = 1000 000 微妙 = 10^9纳秒std::cout << username << " 正在进行抢票: " << tickets << std::endl;tickets--;pthread_mutex_unlock(&lock);}else{pthread_mutex_unlock(&lock);break;}}return nullptr;
}

运行现象:
在这里插入图片描述
我们可以很明显感受到,运行的速度变慢了,而且这里的线程4一直在抢票,别的线程没有机会

  • 加锁和解锁的过程多个线程串行执行的,程序变慢了!
  • 锁只规定互斥访问,没有规定必须让谁优先执行
  • 锁就是真是的让多个执行流进行竞争的结果

线程解锁后,立马又申请锁,导致别的线程竞争不过,我们可以在每次循环末尾增加一段阻塞时间:
在这里插入图片描述

如何看待锁?

  1. 锁本身就是一个共享资源(我们一份代码要使用锁,前提是我们能看到这个锁,这个资源),全局的变量是要被保护的,锁是用来保护全局的资源的,锁本身也是全局资源,锁的安全谁来保护呢?
  2. pthread_mutex_lock、pthread_mutex_unlock:加锁的过程必须是安全的!加锁的过程其实是原子的!
  3. 如果申请成功,就继续向后执行,如果申请暂时没有成功,执行流会阻塞!
  4. 谁持有锁,谁进入临界区!

对锁的思考:
在这里插入图片描述

  • 如果线程1,申请锁成功,进入临界资源,正在访问临界资源期间,其他线程在做什么?阻塞等待
  • 如果线程1,申请锁成功,进入临界资源,正在访问临界资源期间,线程1可不可以被切换呢?绝对可以的。当持有锁的线程被切走的时候,是抱着锁被切走的,即便自己被切走了,其他线程依旧无法申请锁成功,也便无法向后执行,直到我最终释放这个锁!

所以,对于其他线程而言,有意义的锁的状态,无非两种:1.申请锁前2.释放锁后。站在其他线程的角度看待当前线程持有锁的过程,就是原子的!

未来我们在使用锁的时候,一定要尽量保证临界区的粒度(锁中间保护代码的多少)要非常小!

有人可能会想,加锁也未必安全,比如我让线程12加锁去访问公共资源,线程3不加锁去访问公共资源,这样的话公共资源依旧没有被保护起来。加锁是程序员行为,必须做到要加就都要加


互斥量mutex

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

--操作并不是原子操作,而是对应三条汇编指令:

  • load :将共享变量ticket从内存加载到寄存器中
  • update : 更新寄存器里面的值,执行-1操作
  • store :将新值,从寄存器写回共享变量ticket的内存地址

要解决以上问题,需要做到三点:

  1. 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  2. 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  3. 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
    要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量
    在这里插入图片描述

初始化互斥量有两种方法:

  1. 静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  1. 动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrictattr);
参数:
mutex:要初始化的互斥量
attr:NULL

销毁互斥量需要注意:

  • 使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex)

互斥量加锁和解锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误码

调用 pthread_ lock 时,可能会遇到以下情况:

  1. 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  2. 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

锁原理解析

经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题为了实现互斥锁操作,大多数体系结构都提供了swapexchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lockunlock的伪代码改一下

在这里插入图片描述
加锁执行图:
在这里插入图片描述
在这里插入图片描述
接下来的代码就是判断,寄存器中的代码与数据是否符合。

在线程1执行后面的内容时,时刻都可能被切换,但是切换了线程2,寄存器的内容也会被切换掉,这样就算怎么执行,线程2寄存器中始终都是0。再切换回线程1,由上下文保护,寄存器内容切换回原来的数字1。
在这里插入图片描述
解锁:movb $1, mutex:就是将1重新给mutex

这个mutex原本的1就像是一个令牌,它有且只有一个,谁先抢到就能先运行


可重入VS线程安全

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

常见的线程不安全的情况:

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

常见的线程安全的情况:

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可重入的情况:

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构

常见可重入的情况:

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全联系:

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

可重入与线程安全区别:

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

STL中的容器是否是线程安全的?

不是安全的,原因是: STL 的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶)。因此 STL 默认不是线程安全,如果需要在多线程环境下使用,往往需要调用者自行保证线程安全。

智能指针是否是线程安全的?
对于 unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题。
对于 shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证 shared_ptr 能够高效,原子的操作引用计数。


死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

感性认识死锁:小明与小红各自有5毛钱,它们一起去商店买棒棒糖,商店老板说它们的棒棒糖1块钱一个,小明对小红说能不能把她的5毛钱给他,这样他将能凑1块钱买棒棒糖,小红不乐意,她反问小明能不能把他的5毛钱给自己,这样她就能买棒棒糖,它们互相僵持,最后都没买到棒棒糖。他们之前出现的互相僵持的情况就是死锁

死锁四个必要条件:

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

避免死锁方法:

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

避免死锁算法:死锁检测算法、银行家算法


如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀

相关文章:

【Linux】线程互斥 -- 互斥锁 | 死锁 | 线程安全

引入互斥初识锁互斥量mutex锁原理解析 可重入VS线程安全STL中的容器是否是线程安全的? 死锁 引入 我们写一个多线程同时访问一个全局变量的情况(抢票系统)&#xff0c;看看会出什么bug&#xff1a; // 共享资源&#xff0c; 火车票 int tickets 10000; //新线程执行方法 vo…...

【vue-pdf】PDF文件预览插件

1 插件安装 npm install vue-pdf vue-pdf GitHub&#xff1a;https://github.com/FranckFreiburger/vue-pdf#readme 参考文档&#xff1a;https://www.cnblogs.com/steamed-twisted-roll/p/9648255.html catch报错&#xff1a;vue-pdf组件报错vue-pdf Cannot read properti…...

Flink集群运行模式--Standalone运行模式

Flink集群运行模式--Standalone运行模式 一、实验目的二、实验内容三、实验原理四、实验环境五、实验步骤5.1 部署模式5.1.1 会话模式&#xff08;Session Mode&#xff09;5.1.2 单作业模式&#xff08;Per-Job Mode&#xff09;5.1.3 应用模式&#xff08;Application Mode&a…...

Spring整合JUnit实现单元测试

Spring整合JUnit实现单元测试 一、引言 在软件开发过程中&#xff0c;单元测试是保证代码质量和稳定性的重要手段。JUnit是一个流行的Java单元测试框架&#xff0c;而Spring是一个广泛应用于Java企业级开发的框架。本文将介绍如何使用Spring整合JUnit实现单元测试&#xff0c…...

Spring Boot学习路线1

Spring Boot是什么&#xff1f; Spring Boot是基于Spring Framework构建应用程序的框架&#xff0c;Spring Framework是一个广泛使用的用于构建基于Java的企业应用程序的开源框架。Spring Boot旨在使创建独立的、生产级别的Spring应用程序变得容易&#xff0c;您可以"只是…...

管理类联考——写作——论说文——实战篇——标题篇

角度3——4种材料类型、4个立意对象、5种写作态度 经过审题立意后&#xff0c;我们要根据我们的立意&#xff0c;确定一个主题&#xff0c;这个主题必须通过文章的标题直接表达出来。 标题的基本要求 主题清晰&#xff0c;态度明确 第一&#xff0c;阅卷人看到一篇论说文的标…...

idea中设置maven本地仓库和自动下载依赖jar包

1.下载maven 地址&#xff1a;maven3.6.3 解压缩在D:\apache-maven-3.6.3-bin\apache-maven-3.6.3\目录下新建文件夹repository打开apache-maven-3.6.3-bin\apache-maven-3.6.3\conf文件中的settings.xml编辑&#xff1a;新增本地仓库路径 <localRepository>D:\apache-…...

前缀和差分

前缀和 前缀和&#xff1a;一段序列里的前n项和 给出n个数&#xff0c;在给出q次问询&#xff0c;每次问询给出L、R&#xff0c;快速求出每组数组中一段L至R区间的和 给出一段数组&#xff0c;每次问询为求出l到r区间的和 普通方法&#xff1a;L到R进行遍历&#xff0c;那么…...

Golang GORM 模型定义

模型定义 参考文档&#xff1a;https://gorm.io/zh_CN/docs/models.html 模型一般都是普通的 Golang 的结构体&#xff0c;Go的基本数据类型&#xff0c;或者指针。 模型是标准的struct,由Go的基本数据类型、实现了Scanner和Valuer接口的自定义类型及其指针或别名组成&#x…...

微服务的各种边界在架构演进中的作用

演进式架构 在微服务设计和实施的过程中&#xff0c;很多人认为&#xff1a;“将单体拆分成多少个微服务&#xff0c;是微服务的设计重点。”可事实真的是这样吗&#xff1f;其实并非如此&#xff01; Martin Fowler 在提出微服务时&#xff0c;他提到了微服务的一个重要特征—…...

使用 docker-compose 一键部署多个 redis 实例

目录 1. 前期准备 2. 导入镜像 3. 部署redis master脚本 4. 部署redis slave脚本 5. 模板文件 6. 部署redis 7. 基本维护 1. 前期准备 新部署前可以从仓库&#xff08;repository&#xff09;下载 redis 镜像&#xff0c;或者从已有部署中的镜像生成文件&#xff1a; …...

14-测试分类

1.按照测试对象划分 ①界面测试 软件只是一种工具&#xff0c;软件与人的信息交流是通过界面来进行的&#xff0c;界面是软件与用户交流的最直接的一层&#xff0c;界面的设计决定了用户对设计的软件的第一印象。界面如同人的面孔&#xff0c;具有吸引用户的直接优势&#xf…...

打开域名跳转其他网站,官网被黑解决方案(Linux)

某天打开网站&#xff0c;发现进入首页&#xff0c;马上挑战到其他赌博网站。 事不宜迟&#xff0c;不能让客户发现&#xff0c;得马上解决 我的网站跳转到这个域名了 例如网站跳转到 k77.cc 就在你们部署的代码的当前文件夹下面&#xff0c;执行下如下命令 find -type …...

redis总结

1.redis redis高性能的key-value数据库&#xff0c;支持持久化&#xff0c;不仅仅支持简单的key-value&#xff0c;还提供了list&#xff0c;set&#xff0c;zset&#xff0c;hash等数据结构的存储&#xff0c;支持数据的备份&#xff08;master-slave模式&#xff09; redis&…...

现代C++中的从头开始深度学习:激活函数

一、说明 让我们通过在C中实现激活函数来获得乐趣。人工神经网络是生物启发模型的一个例子。在人工神经网络中&#xff0c;称为神经元的处理单元被分组在计算层中&#xff0c;通常用于执行模式识别任务。 在这个模型中&#xff0c;我们通常更喜欢控制每一层的输出以服从一些约束…...

python怎么实现tcp和udp连接

目录 什么是tcp连接 什么是udp连接 python怎么实现tcp和udp连接 什么是tcp连接 TCP&#xff08;Transmission Control Protocol&#xff09;连接是一种网络连接&#xff0c;它提供了可靠的、面向连接的数据传输服务。 在TCP连接中&#xff0c;通信的两端&#xff08;客户端和…...

java设计模式-观察者模式(jdk内置)

上一篇我们学习了 观察者模式。 观察者和被观察者接口都是我们自己定义的&#xff0c;整个设计模式我们从无到有都是自己设计的&#xff0c;其实&#xff0c;java已经内置了这个设计模式&#xff0c;我们只需要定义实现类即可。 下面我们不多说明&#xff0c;直接示例代码&am…...

秒级体验本地调试远程 k8s 中的服务

点击上方蓝色字体&#xff0c;选择“设为星标” 回复”云原生“获取基础架构实践 背景 在这个以k8s为云os的时代&#xff0c;程序员在日常的开发过程中&#xff0c;肯定会遇到各种问题&#xff0c;比如&#xff1a;本地开发完&#xff0c;需要部署到远程k8s集群&#xff0c;本地…...

CV前沿方向:Visual Prompting 视觉提示工程下的范式

prompt在视觉领域&#xff0c;也越来越重要&#xff0c;在图像生成&#xff0c;作为一种可控条件&#xff0c;增进交互和可控性&#xff0c;在多模态理解方面&#xff0c;指令prompt也使得任务灵活通用。视觉提示工程&#xff0c;已然成为CV一个前沿方向&#xff01; 下面来看看…...

Redis五大基础类型解析

1.String类型 特征&#xff1a;即存储字符串的类型&#xff0c;单个字符串存储量最大不超过512MB 常用业务场景&#xff1a;⽤来存储JSON序列化之后对象 底层编码&#xff1a; int编码 数据结构特点&#xff1a;ptr指针直接指向字符串常量池中对应字符串地址&#xff0c;而…...

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周&#xff0c;有很多同学在写期末Java web作业时&#xff0c;运行tomcat出现乱码问题&#xff0c;经过多次解决与研究&#xff0c;我做了如下整理&#xff1a; 原因&#xff1a; IDEA本身编码与tomcat的编码与Windows编码不同导致&#xff0c;Windows 系统控制台…...

零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?

一、核心优势&#xff1a;专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发&#xff0c;是一款收费低廉但功能全面的Windows NAS工具&#xff0c;主打“无学习成本部署” 。与其他NAS软件相比&#xff0c;其优势在于&#xff1a; 无需硬件改造&#xff1a;将任意W…...

TDengine 快速体验(Docker 镜像方式)

简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能&#xff0c;本节首先介绍如何通过 Docker 快速体验 TDengine&#xff0c;然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker&#xff0c;请使用 安装包的方式快…...

css实现圆环展示百分比,根据值动态展示所占比例

代码如下 <view class""><view class"circle-chart"><view v-if"!!num" class"pie-item" :style"{background: conic-gradient(var(--one-color) 0%,#E9E6F1 ${num}%),}"></view><view v-else …...

【2025年】解决Burpsuite抓不到https包的问题

环境&#xff1a;windows11 burpsuite:2025.5 在抓取https网站时&#xff0c;burpsuite抓取不到https数据包&#xff0c;只显示&#xff1a; 解决该问题只需如下三个步骤&#xff1a; 1、浏览器中访问 http://burp 2、下载 CA certificate 证书 3、在设置--隐私与安全--…...

令牌桶 滑动窗口->限流 分布式信号量->限并发的原理 lua脚本分析介绍

文章目录 前言限流限制并发的实际理解限流令牌桶代码实现结果分析令牌桶lua的模拟实现原理总结&#xff1a; 滑动窗口代码实现结果分析lua脚本原理解析 限并发分布式信号量代码实现结果分析lua脚本实现原理 双注解去实现限流 并发结果分析&#xff1a; 实际业务去理解体会统一注…...

C++八股 —— 单例模式

文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全&#xff08;Thread Safety&#xff09; 线程安全是指在多线程环境下&#xff0c;某个函数、类或代码片段能够被多个线程同时调用时&#xff0c;仍能保证数据的一致性和逻辑的正确性&#xf…...

如何在最短时间内提升打ctf(web)的水平?

刚刚刷完2遍 bugku 的 web 题&#xff0c;前来答题。 每个人对刷题理解是不同&#xff0c;有的人是看了writeup就等于刷了&#xff0c;有的人是收藏了writeup就等于刷了&#xff0c;有的人是跟着writeup做了一遍就等于刷了&#xff0c;还有的人是独立思考做了一遍就等于刷了。…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

C#中的CLR属性、依赖属性与附加属性

CLR属性的主要特征 封装性&#xff1a; 隐藏字段的实现细节 提供对字段的受控访问 访问控制&#xff1a; 可单独设置get/set访问器的可见性 可创建只读或只写属性 计算属性&#xff1a; 可以在getter中执行计算逻辑 不需要直接对应一个字段 验证逻辑&#xff1a; 可以…...