0

0

Go语言中内存重排现象的观察与GOMAXPROCS的作用

霞舞

霞舞

发布时间:2025-11-09 13:05:01

|

144人浏览过

|

来源于php中文网

原创

Go语言中内存重排现象的观察与GOMAXPROCS的作用

本文探讨了在go语言中复现内存重排现象的挑战,并解释了为何在特定条件下难以观察到这种行为。核心原因是go运行时对并发任务的调度策略,特别是gomaxprocs参数的设置。文章将通过示例代码分析,阐明gomaxprocs如何影响并发执行,以及go 1.5版本后该参数的默认行为变化,最后强调go内存模型与并发安全实践。

在现代多核处理器系统中,为了提高性能,编译器和CPU常常会对指令进行重排序。这种内存重排(Memory Reordering)在单线程环境下通常是不可见的,但在并发编程中,它可能导致共享数据出现非预期状态,从而引发难以调试的错误。Preshing的“Memory Reordering Caught in the Act”博客提供了一个经典的C++示例,用于演示如何观察到这种现象。然而,当尝试在Go语言中复现类似实验时,许多开发者发现内存重排现象难以被观察到。

Go语言中的内存重排实验与观察

为了理解Go语言中内存重排的特性,我们可以构建一个经典的并发场景:两个并发执行的实体(在Go中是goroutine),各自写入一个共享变量,然后读取另一个共享变量。如果发生内存重排,即写操作和读操作的顺序被优化调换,就有可能观察到两个读取操作都看到了变量的初始值。

以下是Preshing示例在Go语言中的实现:

package main

import (
    "fmt"
    "math/rand"
    "runtime" // 引入runtime包以操作GOMAXPROCS
)

var x, y, r1, r2 int
var detected = 0

// randWait 模拟一些不确定的工作负载,增加调度和重排的机会
func randWait() {
    for rand.Intn(8) != 0 {
    }
}

func main() {
    // 在Go 1.5之前的版本,需要显式设置GOMAXPROCS以利用多核
    // runtime.GOMAXPROCS(runtime.NumCPU()) 

    beginSig1 := make(chan bool, 1)
    beginSig2 := make(chan bool, 1)
    endSig1 := make(chan bool, 1)
    endSig2 := make(chan bool, 1)

    // Goroutine 1
    go func() {
        for {
            <-beginSig1
            randWait()
            x = 1 // 写入 x
            r1 = y // 读取 y
            endSig1 <- true
        }
    }()

    // Goroutine 2
    go func() {
        for {
            <-beginSig2
            randWait()
            y = 1 // 写入 y
            r2 = x // 读取 x
            endSig2 <- true
        }
    }()

    // 主循环,不断重置变量并启动goroutine
    for i := 1; ; i = i + 1 {
        x = 0
        y = 0
        beginSig1 <- true
        beginSig2 <- true
        <-endSig1
        <-endSig2

        // 如果 r1 和 r2 都为 0,则表明可能发生了内存重排
        // 即 goroutine 1 在 x=1 之前读取了 y=0,
        // 且 goroutine 2 在 y=1 之前读取了 x=0。
        if r1 == 0 && r2 == 0 {
            detected = detected + 1
            fmt.Println(detected, "reorders detected after ", i, "iterations")
        }
    }
}

在这个实验中,如果r1和r2都为0,则表示在两个goroutine分别将x和y设置为1之前,它们都读取到了对方变量的初始值0。这通常是内存重排的一个标志。然而,在许多情况下,运行上述代码可能永远不会打印出“reorders detected”的消息。

立即学习go语言免费学习笔记(深入)”;

Go语言中内存重排难以观察的根本原因

最初,一些开发者可能会怀疑Go编译器生成的汇编代码中是否存在特殊的指令(例如示例中观察到的dec eax)阻止了内存重排。然而,根据Intel处理器架构手册,dec eax这类普通指令并非内存屏障,它们本身不能阻止内存重排。将这类指令添加到C代码中也无法消除内存重排现象。

导致Go语言中内存重排难以观察的真正关键在于Go运行时的调度策略,特别是GOMAXPROCS环境变量或runtime.GOMAXPROCS()函数的设置。

Groq
Groq

GroqChat是一个全新的AI聊天机器人平台,支持多种大模型语言,可以免费在线使用。

下载
  1. GOMAXPROCS的作用: GOMAXPROCS控制着Go调度器能够同时使用的操作系统线程(P,Processor)数量。每个P可以运行一个或多个goroutine。
  2. 单CPU核心执行: 在Go 1.5版本之前,GOMAXPROCS的默认值是1。这意味着Go运行时将所有的goroutine(包括我们示例中的两个)都调度到单个操作系统线程上执行。即使程序逻辑上是并发的,它们在物理上却是在同一个CPU核心上交替执行的。在这种串行执行模式下,CPU级别的内存重排跨不同核心的现象将不会发生,因为只有一个核心在工作。因此,即使CPU或编译器进行了指令重排,其效果也无法在多个核心之间被观察到。
  3. Go 1.5+版本后的行为变化: 从Go 1.5版本开始,GOMAXPROCS的默认值变更为机器上的逻辑CPU数量(即runtime.NumCPU())。这一重要改变意味着,在Go 1.5及更高版本中,Go程序默认就能利用多核处理器并行执行goroutine。在这种情况下,上述实验代码就更有可能在多个CPU核心之间观察到内存重排现象,因为goroutine现在可以真正地并行运行,并且不同核心的内存访问顺序可能因重排而变得不可预测。

因此,如果你使用的是Go 1.5之前的版本,并且没有显式调用runtime.GOMAXPROCS(runtime.NumCPU()),那么你很可能无法观察到内存重排。而在Go 1.5及更高版本中,由于默认设置已利用多核,观察到内存重排的可能性大大增加。

Go内存模型与并发编程实践

虽然通过调整GOMAXPROCS可以观察到内存重排,但这通常是一种诊断工具,而非编写并发程序的策略。Go语言提供了明确的内存模型(Go Memory Model),它定义了在并发程序中,一个goroutine的写操作何时能被另一个goroutine观察到(即“happens-before”关系)。

为了编写正确、健壮且可预测的并发程序,开发者应该始终依赖Go提供的同步原语,而不是依赖于或试图利用未受保护的共享内存访问可能导致的内存重排行为。

Go语言推荐的并发编程范式主要包括:

  • 通道 (Channels):这是Go语言并发的核心,用于goroutine之间安全地传递数据和进行同步。通过通道进行通信是Go并发编程的首选方式,它能自然地建立“happens-before”关系,从而避免数据竞争。
  • 互斥锁 (Mutexes):sync.Mutex用于保护共享资源的访问。当多个goroutine需要访问同一块内存区域时,可以使用互斥锁来确保同一时间只有一个goroutine能够访问,从而避免数据竞争。
  • 原子操作 (Atomic Operations):sync/atomic包提供了一组原子操作,用于对基本数据类型(如整数、指针)进行原子性的读、写、增、减和比较交换等操作。这些操作保证了在多核环境下的可见性和顺序性。

总结与注意事项

Go语言中内存重排现象的观察,主要受到GOMAXPROCS设置的影响。在Go 1.5之前,由于GOMAXPROCS默认为1,导致goroutine在单核上串行执行,从而难以观察到跨核心的内存重排。Go 1.5及更高版本将GOMAXPROCS默认设置为逻辑CPU数量,使得这类实验更容易复现内存重排。

然而,无论是否能观察到内存重排,一个重要的编程原则是:永远不要依赖于内存重排的出现或不出现。为了确保并发程序的正确性和数据一致性,开发者必须始终使用Go提供的同步原语(如通道、互斥锁、原子操作)来明确地协调goroutine之间的内存访问。理解GOMAXPROCS的原理有助于调试和优化Go并发程序的性能,但在日常开发中,应将精力集中在编写遵循Go内存模型和同步规则的并发代码上。

相关专题

更多
数据类型有哪几种
数据类型有哪几种

数据类型有整型、浮点型、字符型、字符串型、布尔型、数组、结构体和枚举等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

298

2023.10.31

php数据类型
php数据类型

本专题整合了php数据类型相关内容,阅读专题下面的文章了解更多详细内容。

216

2025.10.31

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

472

2023.08.10

Go中Type关键字的用法
Go中Type关键字的用法

Go中Type关键字的用法有定义新的类型别名或者创建新的结构体类型。本专题为大家提供Go相关的文章、下载、课程内容,供大家免费下载体验。

233

2023.09.06

go怎么实现链表
go怎么实现链表

go通过定义一个节点结构体、定义一个链表结构体、定义一些方法来操作链表、实现一个方法来删除链表中的一个节点和实现一个方法来打印链表中的所有节点的方法实现链表。

442

2023.09.25

go语言编程软件有哪些
go语言编程软件有哪些

go语言编程软件有Go编译器、Go开发环境、Go包管理器、Go测试框架、Go文档生成器、Go代码质量工具和Go性能分析工具等。本专题为大家提供go语言相关的文章、下载、课程内容,供大家免费下载体验。

246

2023.10.13

0基础如何学go语言
0基础如何学go语言

0基础学习Go语言需要分阶段进行,从基础知识到实践项目,逐步深入。php中文网给大家带来了go语言相关的教程以及文章,欢迎大家前来学习。

691

2023.10.26

Go语言实现运算符重载有哪些方法
Go语言实现运算符重载有哪些方法

Go语言不支持运算符重载,但可以通过一些方法来模拟运算符重载的效果。使用函数重载来模拟运算符重载,可以为不同的类型定义不同的函数,以实现类似运算符重载的效果,通过函数重载,可以为不同的类型实现不同的操作。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

187

2024.02.23

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

74

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Go 教程
Go 教程

共32课时 | 3.2万人学习

Go语言实战之 GraphQL
Go语言实战之 GraphQL

共10课时 | 0.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号