浅析golang如何在多线程中避免CPU指令重排

 更新时间:2024年03月27日 16:42:59   作者:Cao Lilu  
这篇文章主要为大家详细介绍了golang在多线程中避免CPU指令重排的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

起因

golang 的发明初衷便是多线程,是一门专门用于多线程高并发的编程语言。其独创的 GMP 模型在多线程的开发上提供了很大的便利。

现代计算机基本上都是多核 CPU 的结构。CPU 在进行指令运行的时候,为了提高效率,会在一些情况下对指令进行重排序,其目的是在保持运行结果和不重拍序的指令一致的前提下,提高程序的运行效率。但是对于多线程并行执行来说,我们可能需要对此额外关注,以避免重排对多线程的影响。

英特尔在其 x86/64 体系结构规范第 3 卷 §8.2.3 中列出了几个这样的问题。这里有一个最简单的例子。假设内存中有两个整数 X 和 Y,最初的值都是 0。两个并行运行的处理器执行以下的机器代码:

虽然在这个例子中使用汇编语言,但这确实是说明 CPU 排序的比较好的方式。每个处理器将 1 存储到其中一个整数变量中,然后将另一个整数加载到寄存器中。(r1 和 r2 只是实际 x86 寄存器(如 eax)的占位符名称。)

现在,无论哪个处理器先将 1 写入内存,都很自然地希望另一个处理器读取回该值,这意味着我们最终应该得到 r1=1、r2=1,或者两者都有。但根据英特尔的规范,情况不一定如此。在规范中,在这个例子的结尾,r1 和 r2 都等于 0 是合法的!这可能是一个违反直觉的结果!

理解这一点的一种方法是,与大多数处理器系列一样,英特尔x86/64处理器可以根据某些规则重新排序机器指令的内存交互,只要它永远不会改变单线程程序的执行。特别地,允许每个处理器将存储的效果延迟超过来自不同位置的任何加载。因此,最终可能会出现指令按以下顺序执行的情况:

程序测试

CPU 指令重排导致的问题

在下面的程序中,来实现上述 CPU 指令重排在多线程中造成的数据不一致现象。下面代码中,声明了 a,b,x,y 四个变量并将其默认值设置为 0。声明两个 go routine 分别执行目标操作(见代码)。正常情况,不管下面 a = 1,x = b,b = 1, y = a 这四条质量如何执行,如果没有重排产生,那么永远不可能出现 x == 0 和 y == 0 同时发生的情况。

但是由于 CPU 指令重排的原因,在实际执行的情况下,在第 1738, 110002, 12987 次测试到了 CPU 指令重排的发生。

func withCpuReordering() {
	index := 0
	for {
		index += 1

		var a, b int32 = 0, 0
		var x, y int32 = 0, 0

		var wg sync.WaitGroup
		wg.Add(2)

		go func() {
			defer wg.Done()

			a = 1
			x = b
		}()

		go func() {
			defer wg.Done()

			b = 1
			y = a
		}()
		wg.Wait()

		if x == 0 && y == 0 {
			panic("CPU Reordering occurs!")
		} else {
			fmt.Println("Now processing in loop", index)
		}
	}
}

绑定 CPU 消除指令重排

上述例子的现象只在多核 CPU 执行的之后才会出现,也就是线程并行执行的时候才会出现。如果我们将上述程序的执行都锁定在一个 CPU 上,也就能避免这种情况的发生。

在下面代码中,我们制定 go routine 最多只能使用一个 CPU。在整个测试过程中,没有出现 x == 0 和 y == 0 同时发生的情况。

func main() {
	runtime.GOMAXPROCS(1)
	withCpuReordering()
}

原因在于指令重排的目的在于提高执行效率,而不是改变执行结果。

通过内存屏障消除指令重排

在 Go 语言的 sync/atomic 包中,原子操作函数的实现会使用 CPU 提供的原子操作指令,以实现对共享变量的原子读写操作。这些原子操作指令通常会在硬件层面实现内存屏障(Memory Barrier),以确保对共享变量的读写操作在不同的 CPU 核心之间具有一定的有序性。

在下面的代码中,通过 atomic 包中的原子操作函数代替了上述代码中的赋值操作,从而解决了执行结果不一致的情况。

func withoutCpuReordering() {
	index := 0
	for {
		index += 1

		var a, b int32 = 0, 0
		var x, y int32 = 0, 0

		var wg sync.WaitGroup
		wg.Add(2)

		go func() {
			defer wg.Done()

			atomic.StoreInt32(&a, 1)
			atomic.StoreInt32(&x, atomic.LoadInt32(&b))
		}()

		go func() {
			defer wg.Done()

			atomic.StoreInt32(&b, 1)
			atomic.StoreInt32(&y, atomic.LoadInt32(&a))
		}()
		wg.Wait()

		if x == 0 && y == 0 {
			panic("CPU Reordering occurs!")
		} else {
			fmt.Println("Now processing in loop", index)
		}
	}
}

类似的指令和不同的平台

所有这些不同的 CPU 系列,每个都有独特的指令来强制执行内存排序,编译器根据不同的 CPU 系列将代码编译成不同的指令,并且每个跨平台项目都实现了自己的可移植层。这些都无助于简化无锁编程!这也是最近引入 C++11 原子库标准的部分原因。这是一种标准化的尝试,使编写可移植的无锁代码变得更容易。

比如 mfence 指令特定于 x86/64 的 CPU 架构。如果想使代码更具可移植性,可以将此内在特性封装在预处理器宏中。Linux 内核将其封装在一个名为 smp_mb 的宏,以及相关的宏中,如 smp_rmb 和 smp_wmb,并在不同的体系结构上提供了替代实现。例如,在 PowerPC 上,smp_mb 被实现为 sync。

到此这篇关于浅析golang如何在多线程中避免CPU指令重排的文章就介绍到这了,更多相关go多线程避免CPU指令重排内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Go语言开发快速学习CGO编程

    Go语言开发快速学习CGO编程

    这篇文章主要为大家介绍了Go语言开发之快速学习CGO编程,看了本文你就会发现CGO编程其实没有想象的那么难,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-04-04
  • 一文带你了解Go语言实现的并发神库conc

    一文带你了解Go语言实现的并发神库conc

    前几天逛github发现了一个有趣的并发库-conc,这篇文章将为大家详细介绍一下这个库的实现,文中的示例代码讲解详细,感兴趣的可以了解一下
    2023-01-01
  • 详解玩转直播系列之消息模块演进

    详解玩转直播系列之消息模块演进

    本篇文章针对秀场直播,简单地描述一下消息模型,说明一下我们消息模型的架构,并结合我们一年以来,通过处理不同的业务线上问题,来进行演进式的消息模型架构的升级与调整,将此整理成文,并分享給大家
    2021-06-06
  • go强制类型转换type(a)以及范围引起的数据差异

    go强制类型转换type(a)以及范围引起的数据差异

    这篇文章主要为大家介绍了go强制类型转换type(a)以及范围引起的数据差异,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-10-10
  • Go语言学习之Switch语句的使用

    Go语言学习之Switch语句的使用

    这篇文章主要通过一些示例为大家介绍一下Go语言中Switch语句的基本语法以及使用,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下
    2022-06-06
  • 利用golang进行OpenCV学习和开发的步骤

    利用golang进行OpenCV学习和开发的步骤

    目前,OpenCV逐步成为一个通用的基础研究和产品开发平台,下面这篇文章主要给大家介绍了关于利用golang进行OpenCV学习和开发的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
    2018-09-09
  • go语言基础 seek光标位置os包的使用

    go语言基础 seek光标位置os包的使用

    这篇文章主要介绍了go语言基础 seek光标位置os包的使用详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-05-05
  • Go中跨域Cors中间件的实现

    Go中跨域Cors中间件的实现

    本文主要介绍了Go中跨域Cors中间件的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-06-06
  • Go语言模拟while语句实现无限循环的方法

    Go语言模拟while语句实现无限循环的方法

    这篇文章主要介绍了Go语言模拟while语句实现无限循环的方法,实例分析了for语句模拟while语句的技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-02-02
  • 实用的Go语言开发工具及使用示例

    实用的Go语言开发工具及使用示例

    这篇文章主要为大家介绍了实用的Go语言开发工具及使用示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-06-06

最新评论