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

可重入函数与线程安全

指令乱序和线程安全

先来看什么是指令乱序问题以及为什么有指令乱序。程序的代码执行顺序有可能被编译器或CPU根据某种策略打乱指令执行顺序,目的是提升程序的执行性能,让程序的执行尽可能并行,这就是所谓指令乱序问题。理解指令乱序的策略是很重要的,因为软件设计人员可以在正确的位置告诉编译器或CPU哪里可以允许指令乱序,哪里不能接受指令乱序,从而在保证软件正确性的同时允许编译或执行层面的性能优化。

指令乱序问题需要分为三个层次:

  • 第1层是多线程编程中的业务逻辑层面的函数可重入性和线程安全问题;
  • 第2层是编译器编译优化造成的指令乱序;
  • 第3层是CPU乱序执行指令的问题。

我们在讨论CPU指令乱序问题和编译器指令乱序问题之前,先来简要讨论一下可重入函数与线程安全相关的问题。

可重入函数与线程安全

线程的基本概念

线程(thread)是操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程中的实际运作单位。一个线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一般默认一个进程中只包含一个线程。

操作系统中的线程概念也被延伸到CPU硬件上,多线程CPU就是在一个CPU上支持同时运行多个指令流,而多核CPU就是在一块芯片上集成了多个CPU核,比如4核8线程CPU芯片就是在集成了4个CPU核,每个CPU核上支持2个线程。

有了多核多线程CPU,操作系统就可以让不同进程运行在不同的CPU核的不同线程上,从而大大减少进程调度进程切换的资源消耗。传统上操作系统工作在单核单线程CPU上是通过分时共享CPU来模拟出多个指令执行流,从而实现多进程和多线程的。

函数调用堆栈框架

借助函数调用堆栈可以将我们写的函数调用代码整理成一个顺序执行的指令流,也就是一个线程,每一个线程都有一个独自拥有的函数调用堆栈空间,其中函数参数和局部变量都存储在函数调用堆栈空间中,因此函数参数和局部变量也是线程独自拥有的。除了函数调用堆栈空间,同一个进程的多个线程是共享其他进程资源的,比如全局变量是多个线程共享的。

可重入函数

可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用局部变量,要么在使用全局变量时保护自己的数据。

int g = 0;
int function()
{g++; /* switch to another thread */printf("%d", g);
}int function2(int a)
{a++;printf("%d", a); 
}

function()函数为不可重入函数,其中的变量g为全局变量,多个线程同时执行function函数时会出现变量g的值未按照预想的结果输出的情况,function2(int a)为可重入函数,function2函数中的变量a是对传入的实参变量的拷贝,并不影响原来传入的变量。

可重入函数的基本要求

(1)不为连续的调用持有静态数据;     

(2)不返回指向静态数据的指针;     

(3)所有数据都由函数的调用者提供;     

(4)使用局部变量,或者通过制作全局数据的局部变量拷贝来保护全局数据;  

(5)使用静态数据或全局变量时做周密的并行时序分析,通过临界区互斥避免临界区冲突;     

(6)绝不调用任何不可重入函数。

什么是线程安全?

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

函数的可重入性与线程安全之间的关系

可重入的函数不一定是线程安全的,可能是线程安全的也可能不是线程安全的;同一个可重入的函数在多个线程中并发使用时是线程安全的,但不同的可重入函数(共享全局变量及静态变量)在多个线程中并发使用时会有线程安全问题;不可重入的函数一定不是线程安全的。

int g = 0;
int plus()
{pthread_mutex_lock(&gplusplus);g++; /* switch to another thread */printf("%d", g);pthread_mutex_unlock(&gplusplus);
}
int minus()
{pthread_mutex_lock(&gminusminus);g--; /* switch to another thread */printf("%d", g);pthread_mutex_unlock(&gminusminus);
}

上述两个函数plus() 和 minus() 经过加锁处理之后均称为可重入函数,但由于使用的是两个不同的互斥锁,所以在并发执行时会出现g的值与预期不一致的现象。故说明可重入函数不一定是线程安全的。

线程安全和指令乱序

我们这里讨论可重入函数与线程安全本质上也是指令乱序执行问题,指令乱序问题本质上也是线程安全问题,编译器编译优化或CPU指令乱序执行所引发的程序正确性问题尽管所处的层次不同但本质上与此相似,接下来我们分别讨论一下CPU指令乱序问题和编译器指令乱序问题。

CPU的流水线技术能够让指令的执行尽可能地并行起来,但是如果两条指令前后存在依赖关系,比如数据依赖、控制依赖等,此时后一条指令就必需等到前一条指令完成后才能开始执行。为了提高流水线的运行效率,CPU会对无依赖的前后指令做适当的乱序和调整,对控制依赖的指令做分支预测,对内存访问等耗时操作提前预先处理等,这些都会导致指令乱序执行。

编译器很重要的一项工作就是优化我们的代码以提高性能。这包括在不改变程序正确性的条件下重新排列指令,也就是编译器指令乱序问题。

CPU指令执行的顺序一致性

为了提高流水线的运行效率,CPU会对无依赖的前后指令做适当的乱序和调整,对控制依赖的指令做分支预测,对内存访问等耗时操作提前预先处理等,这些都会导致指令乱序执行。

但是我们编程时一般理解代码在CPU上的执行顺序和代码的逻辑顺序是一致的呀?这有点让人困惑。从单核单线程CPU的角度来看,指令在CPU内部可能是乱序执行的,但是对外表现却是顺序执行的。因为指令集架构(ISA)中的指令寄存器作为CPU的对外接口,CPU只需要把内部真实的物理寄存器按照指令的执行顺序,顺序映射到ISA寄存器上,也就是CPU只要将结果顺序地提交到ISA寄存器,就可以保证顺序一致性(Sequential consistency)

多核CPU上指令乱序执行

显然在单核单线程CPU上指令乱序问题被指令集架构所屏蔽,但是在多核多线程CPU上依然存在指令乱序执行的可能性。比如存在变量x = 0,CPU0上执行写入操作x = 1。接着在CPU1上,执行读取操作依然得到x = 0,这在X86和ARM多核CPU上都是可能出现的。原因是如图所示CPU核和Cache以及内存之间,存在着Store Buffer,当x = 1执行写入操作成功后,修改只存在于Store Buffer中,并未写到cache以及内存上,因此CPU1读取不到最新的x值。除了Store Buffer,而且还可能会有Invalidate Queue,导致CPU1读不到最新的x值。为了能够保证多核之间的修改可见性,我们在写程序的时候需要加上内存屏障,例如X86上的mfence指令。

ARM64 CPU指令乱序

对于ARM64架构的CPU来说,编程就变得危险多了。除了存在数据依赖、控制依赖和地址依赖等不能被乱序执行外,其余指令间都有可能存在乱序执行。ARM64上没有依赖关系的读后读、写后写、读后写和写后读都是可以乱序执行的。ARM64架构下Store Buffer并不是FIFO的,而且还可能存在Invalidate Queue,这让并发编程变得困难重重。总之ARM64是弱内存序模型因为精简指令集把访存指令和运算指令分开了,为了性能允许几乎所有的指令乱序,但前提是不影响程序的正确性。因此ARM64架构的指令乱序问题需要引入不同类型的barrier来保证程序的正确性。

需要特别指出的是ARM64允许指令乱序执行是出于性能的考虑,这是架构特性,不是漏洞。但是指令乱序的影响却给系统可靠性带来了风险,驱动模块、基础软件和应用软件都要做排查和设计优化。

高级语言定义了逻辑关系,逻辑关系与应用程序的业务逻辑有关;编译器将内含逻辑关系的高级语言代码翻译成机器语言或汇编语言,其中就定义了数据依赖、控制依赖和地址依赖等依赖关系;ARMv8架构定义了内存模型以及实现处理这些依赖关系的机器语言指令,从而防止有依赖的指令乱序执行影响程序正确性。

显然CPU指令乱序与硬件内存模型及防止指令乱序的机器语言指令内部实现紧密相关,这些需要深入到处理器微架构深处才能一探究竟,与我们专注于Linux内核的目标不符,这里不再深入探讨它。但是我们需要清楚的一点是,CPU仅能看到机器指令或汇编指令序列中的数据依赖、控制依赖和地址依赖等依赖关系,并不能理解高级语言中定义的逻辑关系,因此CPU指令乱序执行和编译优化指令乱序都可能会破坏高级语言中定义的逻辑关系,这是我们学习指令乱序问题的原因。

编译器指令乱序问题

编译器很重要的一项工作就是优化我们的代码以提高性能。这包括在不改变程序正确性的条件下重新排列指令,也就是编译器指令乱序问题。

因为编译器不知道什么样的代码需要线程安全,所以编译器假设代码都是单线程执行的,也就是编译器对函数的可重入问题是没有感知的,因此编译器进行指令重排优化只能保证是单线程安全。因此当多线程应用程序的逻辑关系在编译器重新排序指令的时候可能影响程序正确性时,除非你显式告诉编译器,我不需要重排指令顺序,否则编译器可能会在优化指令顺序时影响程序的正确性。这一部分我们一起探究编译器编译优化相关的指令乱序问题。

编译器屏障

在阅读Linux内核源代码时,会看到额外插入的汇编指令如下,是告诉编译器不要优化指令顺序。如下代码摘自Linux内核源代码include/linux/compiler-gcc.h。

#define barrier() __asm__ __volatile__("": : :"memory")

如上代码定义的宏barrier()就是常说的编译器屏障(compiler barriers),它的主要用途就是告诉编译器不要优化重排指令顺序。为了说明这个问题我们用C语言代码及对应的ARM64汇编代码简要说明指令乱序造成的问题及编译器屏障的作用。

编译器优化造成指令乱序问题

编译器的主要工作就是将高级语言源代码翻译成机器指令,当然翻译的过程中编译器还会进行编译优化以提高代码的执行效率。编译优化主要就是在不影响程序正确性的情况下对机器指令顺序重排从而统筹调度CPU资源改善程序性能,但是对于多线程应用程序编译器并不能理解程序的并发执行逻辑,很可能会好心干坏事。为了说明编译优化指令乱序造成的问题,我们考虑下面的compiler_reordering.c文件中C语言函数function的代码。

int flag, data;int function(void)
{data = data + 1;flag = 1;
} 

编译时未开启编译优化 

gcc -S compiler_reordering.c -o compiler_reordering.s
function:adrp	x0, :got:dataldr	x0, [x0, #:got_lo12:data]ldr	w0, [x0] 	// load data to w0add	w1, w0, 1 // w1 = w0 + 1adrp	x0, :got:dataldr	x0, [x0, #:got_lo12:data]str	w1, [x0]	// data = data + 1adrp	x0, :got:flagldr	x0, [x0, #:got_lo12:flag]mov	w1, 1		// w1 = 1str	w1, [x0]	// flag = 1nopret

编译时开启编译优化  

gcc -O2 -S compiler_reordering.c -o compiler_reordering_O2.s
function:adrp	x1, :got:dataadrp	x3, :got:flagmov	w4, 1		// w4 = 1ldr	x1, [x1, #:got_lo12:data]ldr	x3, [x3, #:got_lo12:flag]ldr	w2, [x1]	// load data to w2str	w4, [x3]	// flag = 1add	w2, w2, w4	// w2 = w2 + 1str	w2, [x1]	// data = data + 1ret

与上述C语言函数function中的代码比较,这段优化后的ARM64汇编代码的执行顺序是不同的。C代码中是先存储了data的值,后存储了flag的值,而优化后的ARM64汇编代码正好相反,先存储了flag,后保存了data。

这就是编译器指令乱序问题的典型范例。为什么编译器会这么做呢?对于单线程来说,data和 flag的写入顺序,编译器认为没有任何问题的。并且最终的结果data和flag的值也是正确的。

实际上这种编译器指令乱序问题在大部分情况下是没有问题的。但是在某些情况下可能会引入问题。例如我们使用的全局变量flag标记共享数据data是否就绪。另外一个线程检测到flag == 1就认为data已经就绪,而由于编译器指令乱序,实际上data的值可能还没有存入内存。

下面我们加入内存屏障,再来看看编译后产生的汇编文件。 

#define barrier() __asm__ __volatile__("": : :"memory")int flag, data;int function(void)
{data = data + 1;barrier();flag = 1;
}
function:adrp	x0, :got:dataldr	x0, [x0, #:got_lo12:data]ldr	w1, [x0]		// load data to w1add	w1, w1, 1		// w1 = w1 + 1str	w1, [x0]		// data = data + 1adrp	x1, :got:flagmov	w2, 1			// w2 = 1ldr	x1, [x1, #:got_lo12:flag]str	w2, [x1]		// flag = 1ret

barrier就是编译器提供的内存屏障,作用是告诉编译器内存中的值已经改变,之前对内存的缓存(缓存到寄存器)都需要抛弃,barrier之后的内存操作需要重新从内存加载,而不能使用之前寄存器缓存的值。可以防止编译器优化barrier前后的内存访问顺序。barrier就像是代码中的一道不可逾越的屏障,barrier前的内存读写操作不能跑到barrier后面;同样barrier后面的内存读写操作不能在barrier之前。


以上内容为中科大软件学院《高级软件工程》课后总结,感谢孟宁老师的倾心教授,老师讲的太好啦(^_^)

参考资料:《代码中的软件工程》    孟宁  编著

相关文章:

可重入函数与线程安全

指令乱序和线程安全 先来看什么是指令乱序问题以及为什么有指令乱序。程序的代码执行顺序有可能被编译器或CPU根据某种策略打乱指令执行顺序,目的是提升程序的执行性能,让程序的执行尽可能并行,这就是所谓指令乱序问题。理解指令乱序的策略是…...

一文彻底读懂异地多活

文章目录 系统可用性单机架构主从副本风险不可控同城灾备同城双活两地三中心伪异地双活真正的异地双活如何实施异地双活1、按业务类型分片2、直接哈希分片3、按地理位置分片异地多活总结系统可用性 要想理解异地多活,我们需要从架构设计的原则说起。 现如今,我们开发一个软件…...

孕酮PEG偶联物:mPEG Progestrone,PEG Progestrone,甲氧基聚乙二醇孕酮

中文名称:甲氧基聚乙二醇孕酮 英文名称:mPEG Progestrone,PEG Progestrone 一、反应机理: 孕酮-PEG衍生物是一类具有生物活性的类固醇-PEG偶联物,可用于药物发现或生物测定开发。孕酮是一种女性性激素,负…...

网络系统集成实验(一)| 网络系统集成基础

目录 一、前言 二、实验目的 三、实验需求 四、实验步骤与现象 (1)网络设置、网络命令的使用 ① 在华为设备中,常用指令的使用 ② 在思科设备中,常用指令的使用 ③ 在Windows设备中,常用网络指令的使用 &#xf…...

php composer 如何安装windows电脑

在 Windows 电脑上安装 PHP Composer,你需要按照以下步骤操作: 安装 PHP 确保你的电脑上已经安装了 PHP。如果还没有安装,可以从 PHP 官网(https://www.php.net/downloads.php)下载安装包并安装。 设置环境变量 将 P…...

API 鉴权插件上线!支持用户自定义鉴权插件

0.4.0 版本更新主要围绕这几个方面: 分组独立的 UI,支持分组 API 鉴权 API 测试支持继承 API 鉴权 支持用户自定义鉴权插件,仅需部分配置即可发布鉴权插件 开始介绍功能之前,我想先和大家分享一下鉴权功能设计的一些思考。 其实…...

2023年NOC大赛加码未来编程赛道-初赛-Python(初中组-卷1)

2023年NOC大赛加码未来编程赛道-初赛-Python(初中组-卷1) *1.Python自带的编程环境是? A、PyScripter B、Spyder C、Notepad++ D、IDLE *2.假设a=20,b-3,那么a or b的结果是? () A、20 B、0 C.1 D.3 *3.假设a=2,b=3,那么a-b*b的值是? A、 3 B、-2 C、-7 D、-11 *4.…...

day21—编程题

文章目录1.第一题1.1题目1.2思路1.3解题2.第二题2.1题目2.2思路2.3解题1.第一题 1.1题目 描述: 洗牌在生活中十分常见,现在需要写一个程序模拟洗牌的过程。 现在需要洗2n张牌,从上到下依次是第1张,第2张,第3张一直到…...

【数据结构】栈与队列经典选择题

🚀write in front🚀 📜所属专栏: 🛰️博客主页:睿睿的博客主页 🛰️代码仓库:🎉VS2022_C语言仓库 🎡您的点赞、关注、收藏、评论,是对我最大的激励…...

Linux常用命令详细示例演示

一、Linux 常用命令一览表 Linux 下命令格式: command [-options] [parameter] 命令 [选项] [参数] command 是命令 例如:ls cd copy[-options] 带方括号的都是可选的 一些选项 例如:ls -l 中的 -l[parameter] 可选参数,可以是 0…...

9-数据可视化-动态柱状图

文章目录1.基础柱状图2.基础时间线柱状图3.动态柱状图1.基础柱状图 from pyecharts.charts import Bar bar Bar() # 构建柱状图对象 bar.add_xaxis(["中国","美国","英国"]) bar.add_yaxis("GDP",[30,20,10]) bar.render()反转xy轴…...

Linux系统【centos7x】安装宝塔面板教程

1. 下载宝塔面板安装包 在宝塔官网下载最新版的安装包,下载完后上传到服务器。 2. 安装宝塔面板 在终端中输入以下命令: yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh…...

蓝易云:Linux系统【Centos7】top命令详细解释

top命令是一个非常常用的Linux系统性能监控工具,它可以实时动态地查看系统的各项性能指标,并且可以按照不同的排序方式进行排序,方便用户查找信息。 下面是top命令的详细解释: 1. 第一行:显示系统的运行时间、当前登…...

Muduo库源码剖析(一)——Channel

Muduo库源码剖析(一)——Channel 说明 本源码剖析是在muduo基础上,保留关键部分进行改写分析。 要点总结 事件分发器 event dispatcher中最重要的两个类型 channel 和 Poller Channel可理解为通道,poller往通道传输数据(事件发生情况)。 EventLoop…...

Java多线程:定时器Timer

前言 定时/计划功能在Java应用的各个领域都使用得非常多,比方说Web层面,可能一个项目要定时采集话单、定时更新某些缓存、定时清理一批不活跃用户等等。定时计划任务功能在Java中主要使用的就是Timer对象,它在内部使用多线程方式进行处理&am…...

设计模式---装饰模式

目录 介绍 实现 优缺点 装饰模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有类的一个包装。这种模式创建了一个装饰类,用来包装原有…...

跨时钟域传输数据——单bit和多bit信号(总结)

文章目录前言一、慢时钟域到快时钟域1、单bit信号2、多bit信号二、快时钟域到慢时钟域1、单bit信号2、多bit信号三、多bit信号跨时钟域传输1、多个信号合并2、多周期路径 Multi-cycle Path/MCP3、使用格雷码4、使用异步FIFO5、使用DMUX电路结构6、握手信号传输四、简答题1、跨时…...

高并发下如何保证接口幂等

文章目录 1. insert前先select2. 加悲观锁3. 加乐观锁4. 加唯一索引5. 建防重表6. 根据状态机7. 加分布式锁8. 获取token接口幂等性问题,对于开发人员来说,是一个跟语言无关的公共问题。本文分享了一些解决这类问题非常实用的办法,绝大部分内容我在项目中实践过的,给有需要…...

Retrofit源码分析小结

Retrofit源码分析&小结 简介 Retrofit是对Okhttp网络请求的二次封装,通过注解动态代理的方式,简化了Okhttp的使用,使得通过简单的配置就可以像调用接口一样去请求网络接口;除此之外Retrofit还支持RxJava和kotlin的协程 基本…...

【从零开始学习 UVM】11.4、UVM Register Layer —— UVM Register Model 实战项目(RAL实战,交通灯为例)

文章目录 DesignInterfaceRegister Model ExampleRegister EnvironmentAPB Agent ExampleTestbench EnvironmentSequencesTest在之前的几篇文章中,我们已经了解了寄存器模型是什么以及如何使用它来访问给定设计中的寄存器。现在让我们看一个完整的例子,展示如何为给定设计编写…...

XCTF-web-easyupload

试了试php,php7,pht,phtml等,都没有用 尝试.user.ini 抓包修改将.user.ini修改为jpg图片 在上传一个123.jpg 用蚁剑连接,得到flag...

微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】

微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...

ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放

简介 前面两期文章我们介绍了I2S的读取和写入,一个是通过INMP441麦克风模块采集音频,一个是通过PCM5102A模块播放音频,那如果我们将两者结合起来,将麦克风采集到的音频通过PCM5102A播放,是不是就可以做一个扩音器了呢…...

Unit 1 深度强化学习简介

Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...

AI,如何重构理解、匹配与决策?

AI 时代,我们如何理解消费? 作者|王彬 封面|Unplash 人们通过信息理解世界。 曾几何时,PC 与移动互联网重塑了人们的购物路径:信息变得唾手可得,商品决策变得高度依赖内容。 但 AI 时代的来…...

MySQL 知识小结(一)

一、my.cnf配置详解 我们知道安装MySQL有两种方式来安装咱们的MySQL数据库,分别是二进制安装编译数据库或者使用三方yum来进行安装,第三方yum的安装相对于二进制压缩包的安装更快捷,但是文件存放起来数据比较冗余,用二进制能够更好管理咱们M…...

在Mathematica中实现Newton-Raphson迭代的收敛时间算法(一般三次多项式)

考察一般的三次多项式,以r为参数: p[z_, r_] : z^3 (r - 1) z - r; roots[r_] : z /. Solve[p[z, r] 0, z]; 此多项式的根为: 尽管看起来这个多项式是特殊的,其实一般的三次多项式都是可以通过线性变换化为这个形式…...

基于Java+VUE+MariaDB实现(Web)仿小米商城

仿小米商城 环境安装 nodejs maven JDK11 运行 mvn clean install -DskipTestscd adminmvn spring-boot:runcd ../webmvn spring-boot:runcd ../xiaomi-store-admin-vuenpm installnpm run servecd ../xiaomi-store-vuenpm installnpm run serve 注意:运行前…...

《Docker》架构

文章目录 架构模式单机架构应用数据分离架构应用服务器集群架构读写分离/主从分离架构冷热分离架构垂直分库架构微服务架构容器编排架构什么是容器,docker,镜像,k8s 架构模式 单机架构 单机架构其实就是应用服务器和单机服务器都部署在同一…...

实战设计模式之模板方法模式

概述 模板方法模式定义了一个操作中的算法骨架,并将某些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的前提下,重新定义算法中的某些步骤。简单来说,就是在一个方法中定义了要执行的步骤顺序或算法框架,但允许子类…...