Go:使用共享变量实现并发

竞态

在串行程序中,步骤执行顺序由程序逻辑决定;而在有多个 goroutine 的并发程序中,不同 goroutine 的事件先后顺序不确定,若无法确定两个事件先后,它们就是并发的。若一个函数在并发调用时能正确工作,称其为并发安全。当类型的所有可访问方法和操作都是并发安全时,该类型为并发安全类型。并发安全的类型并非普遍存在,若要在并发中安全访问变量,需限制变量仅在一个 goroutine 内存在,或维护更高层的互斥不变量。

package bankvar balance intfunc Deposit(amount int) { balance = balance + amount }
func Balance() int { return balance }// Alice:
go func() {bank.Deposit(200)  // A1fmt.Println("=", bank.Balance()) // A2
}()
// Bob:
go func() {bank.Deposit(100)  // B
}()

竞态是指多个 goroutine 按交错顺序执行时,程序无法给出正确结果的情形。它对程序是致命的,可能潜伏在程序中,出现频率低,且难以再现和分析。以银行账户程序为例,在并发调用DepositBalance函数时,若多个 goroutine 交错执行,可能出现数据竞态,导致账户余额计算错误,如出现存款丢失等情况。数据竞态发生在两个或多个 goroutine 并发读写同一个变量,且至少其中一个是写入时。当变量类型大于机器字长(如接口、字符串或 slice)时,数据竞态问题会更复杂。

避免数据竞态的方法

  • 不修改变量:对于延迟初始化的 map,若并发调用访问可能存在数据竞态。但如果在创建其他 goroutine 之前,用完整数据初始化 map 且不再修改,那么多个 goroutine 可安全并发调用相关函数读取 map。
package bankvar deposits = make(chan int) // 发送存款额
var balances = make(chan int) // 接收余额func Deposit(amount int) { deposits <- amount }
func Balance() int { return <-balances }func teller() {var balance intfor {select {case amount := <-deposits:balance += amountcase balances <- balance:}}
}func init() {go teller() // 启动监控goroutine
}
  • 避免多个 goroutine 访问同一变量:通过将变量限制在单个 goroutine 内部访问来避免竞态。如 Web 爬虫中主 goroutine 是唯一能访问seen map 的,消息服务器中broadcaster goroutine 是唯一能访问clients map 的。还可通过监控 goroutine 来限制对共享变量的访问,如银行案例中用teller goroutine 限制balance变量的并发访问 。
  • 允许多个 goroutine 访问,但同一时间只有一个可访问:通过互斥机制实现。

互斥锁:sync.Mutex

// 使用通道实现二进制信号量保护balance
var (sema    = make(chan struct{}, 1) // 用来保护 balance 的二进制信号量balance int
)
func Deposit(amount int) {sema <- struct{}{} // 获取令牌balance = balance + amount<-sema // 释放令牌
}
func Balance() int {sema <- struct{}{} // 获取令牌b := balance<-sema // 释放令牌return b
}

为保证同一时间最多有一个 goroutine 能访问共享变量,可使用容量为 1 的通道作为二进制信号量。

由于互斥锁模式应用广泛,Go 语言sync包提供了Mutex类型来支持这种模式,Lock方法用于获取令牌(上锁),Unlock方法用于释放令牌(解锁)。

// 使用sync.Mutex实现互斥锁保护balance
import "sync"
var (mu      sync.Mutex // 保护 balancebalance int
)
func Deposit(amount int) {mu.Lock()balance = balance + amountmu.Unlock()
}
func Balance() int {mu.Lock()b := balancemu.Unlock()return b
}

示例:以银行账户程序为例,定义musync.Mutex类型来保护balance变量 。在DepositBalance函数中,通过先调用mu.Lock()获取互斥锁,访问或修改balance变量,最后调用mu.Unlock()释放锁 ,确保共享变量不会被并发访问 。这种函数、互斥锁、变量的组合方式称为监控(monitor)模式。

func Balance() int {mu.Lock()defer mu.Unlock()return balance
}

LockUnlock之间的代码区域称为临界区域,此区域内可自由读写共享变量 。一个 goroutine 在使用完互斥锁后应及时释放,对于有多个分支(尤其是错误分支)的复杂函数,可使用defer语句延迟执行Unlock,将临界区域扩展到函数结尾,保证锁能正确释放 ,即使在临界区域崩溃时也能正常执行解锁操作 。

原子操作与互斥锁的应用

// 不正确的Withdraw实现示例
func Withdraw(amount int) bool {Deposit(-amount)if Balance() < 0 {Deposit(amount)return false // 余额不足}return true
}// 错误的Withdraw加锁尝试示例
func Withdraw(amount int) bool {mu.Lock()defer mu.Unlock()Deposit(-amount)if Balance() < 0 {Deposit(amount)return false // 余额不足}return true
}// 正确的Withdraw实现示例
func Withdraw(amount int) bool {mu.Lock()defer mu.Unlock()deposit(-amount)if balance < 0 {deposit(amount)return false // 余额不足}return true
}func Deposit(amount int) {mu.Lock()defer mu.Unlock()deposit(amount)
}func Balance() int {mu.Lock()defer mu.Unlock()return balance
}// 这个函数要求已获取互斥锁
func deposit(amount int) { balance += amount }

Withdraw函数为例,最初版本因不是原子操作(包含多个串行操作且未对整个操作上锁)存在问题,在尝试超额提款时可能导致余额异常 。改进版本应在整个操作开始时申请一次互斥锁 ,但直接在Withdraw中嵌套调用已使用互斥锁的Deposit函数会因互斥锁不可再入导致死锁 。最终解决方案是将Deposit函数拆分为不导出的deposit函数(假定已获取互斥锁并完成业务逻辑)和导出的Deposit函数(负责获取锁并调用deposit ),从而正确实现Withdraw函数 。使用互斥锁时,应确保互斥锁本身及被保护的变量都不被导出 ,以维持并发中的不变性

读写互斥锁:sync.RWMutex

var mu sync.RWMutex
var balance intfunc Balance() int {mu.RLock() // 读锁defer mu.RUnlock()return balance
}

以 Bob 频繁查询账户余额为例,银行的Balance函数只是读取变量状态,多个Balance请求可并发运行,只要DepositWithdraw请求不同时运行即可 。为满足这种场景需求,需要一种特殊的锁,即多读单写锁,Go 语言中的sync.RWMutex可提供此功能。

  • 读锁操作:定义musync.RWMutex类型 ,在Balance函数中,通过调用mu.RLock()获取读锁(共享锁),使用defer mu.RUnlock()延迟释放读锁,确保在函数结束时释放锁 ,这样多个读操作可并发进行。
  • 写锁操作Deposit函数等写操作函数,仍通过调用mu.Lock()获取写锁(互斥锁),mu.Unlock()释放写锁 ,保证写操作时的独占访问权限。

注意事项

  • RLock仅适用于临界区域内对共享变量无写操作的情形 ,因为有些看似只读的函数可能会更新内部变量,若不确定应使用独占版本的Lock
  • 当绝大部分 goroutine 都在获取读锁且锁竞争激烈时,RWMutex才有优势,因为其内部簿记工作更复杂,在竞争不激烈时比普通互斥锁慢

内存同步

以银行账户的Balance函数为例,其需要互斥锁不仅是防止操作交错,还涉及内存同步问题。现代计算机多处理器有本地内存缓存,写操作先缓存在处理器中,刷回内存顺序可能与 goroutine 写入顺序不一致。通道通信、互斥锁等同步原语可使处理器将累积写操作刷回内存并提交,保证执行结果对其他处理器上的 goroutine 可见。

var x, y int
go func() {x = 1fmt.Print("y:", y, " ")
}()
go func() {y = 1fmt.Print("x:", x, " ")
}()

通过代码示例,两个 goroutine 并发访问共享变量xy,在未使用互斥锁时存在数据竞态,预期输出为y:0 x:1x:0 y:1x:1 y:1y:1 x:1这四种情况之一 。但实际可能出现x:0 y:0y:0 x:0这种意外输出 。原因在于单个 goroutine 内语句执行顺序一致,但在无同步措施时,不同 goroutine 间无法保证事件顺序一致 。编译器可能因赋值和打印对应不同变量,交换语句执行顺序,CPU 也可能因缓存等问题导致一个 goroutine 的写入操作对另一个 goroutine 的Print语句不可见 。

解决:为避免这些并发问题,可采用成熟模式,将变量限制在单个 goroutine 中;对于其他变量,使用互斥锁进行同步 。

延迟初始化sync.Once

var icons map[string]image.Image
func loadIcons() {icons = map[string]image.Image{"spades.png":  loadIcon("spades.png"),"hearts.png":  loadIcon("hearts.png"),"diamonds.png": loadIcon("diamonds.png"),"clubs.png":   loadIcon("clubs.png"),}
}
// 并发不安全版本
func Icon(name string) image.Image {if icons == nil {loadIcons() // 一次性地初始化}return icons[name]
}

延迟昂贵的初始化步骤到实际需要时进行,可避免增加程序启动延时。以icons变量为例,初始版本在Icon函数中检测icons是否为空,若为空则调用loadIcons进行一次性初始化 ,但此方式在并发调用Icon时不安全。

var mu sync.Mutex // 保护 icons
var icons map[string]image.Image// 并发安全版本(使用普通互斥锁)
func Icon(name string) image.Image {mu.Lock()defer mu.Unlock()if icons == nil {loadIcons()}return icons[name]
}var mu sync.RWMutex // 保护 icons
var icons map[string]image.Image// 并发安全版本(使用读写互斥锁)
func Icon(name string) image.Image {mu.RLock()if icons!= nil {icon := icons[name]mu.RUnlock()return icon}mu.RUnlock()mu.Lock()if icons == nil { // 必须重新检查nil值loadIcons()}icon := icons[name]mu.Unlock()return icon
}

在无显式同步情况下,编译器和 CPU 可能重排loadIcons语句执行顺序,导致一个 goroutine 发现icons不为nil时,初始化可能尚未真正完成 。使用互斥锁可解决同步问题,如用sync.Mutex保护icons变量 ,但这会限制并发访问,即使初始化完成且不再更改,也会阻止多个 goroutine 并发读取 。使用sync.RWMutex虽能改善并发读问题,但代码复杂且易出错 。

var loadIconsOnce sync.Once
var icons map[string]image.Image// 并发安全版本(使用sync.Once)
func Icon(name string) image.Image {loadIconsOnce.Do(loadIcons)return icons[name]
}

sync.Once为一次性初始化问题提供简化方案 。它包含布尔变量记录初始化是否完成,以及互斥量保护相关数据 。OnceDo方法以初始化函数为参数 ,首次调用Do时,锁定互斥量并检查布尔变量,若为假则调用初始化函数并将变量设为真,后续调用相当于空操作 。通过使用sync.Once,可确保变量在正确构造之前不被其他 goroutine 访问,避免竞态问题 。

竞态检测器

Go 语言运行时和工具链提供竞态检测器,用于检测并发编程中的数据竞态问题。在go buildgo rungo test命令中添加-race参数即可启用 。启用后,编译器会构建修改后的版本,记录运行时对共享变量的访问,以及读写变量的 goroutine 标识,还会记录同步事件(如go语句、通道操作、互斥锁调用、WaitGroup调用等 )。

竞态检测器通过研究事件流,找出一个 goroutine 写入变量后,无同步操作时另一个 goroutine 读写该变量的情况,即数据竞态 。检测到竞态后,会输出包含变量标识、读写 goroutine 调用栈的报告,帮助定位问题 。

它只能检测运行时发生的竞态,无法保证程序绝对不会发生竞态 。为获得最佳检测效果,测试应包含并发使用包的场景 。由于增加了额外簿记工作,带竞态检测功能的程序运行时需更长时间和更多内存,但对于排查不常发生的竞态,能节省大量调试时间 。

goroutine 和线程

可增长的栈

每个 OS 线程都有固定大小的栈内存,通常为 2MB ,用于保存在函数调用期间正在执行或临时暂停函数中的局部变量。但这个固定大小存在弊端,对于简单的 goroutine(如仅等待WaitGroup或关闭通道 ),2MB 栈内存浪费;对于复杂深度递归函数,固定大小栈又不够用,且无法兼顾空间效率和支持更深递归。

goroutine 在生命周期开始时栈很小,典型为 2KB ,也用于存放局部变量。与 OS 线程不同,goroutine 的栈可按需增大和缩小,大小限制可达 1GB ,比线程栈大几个数量级,能更灵活适应不同场景,极少的 goroutine 才会用到这么大栈。

goroutine调度

OS 线程由 OS 内核调度。每隔几毫秒,硬件时钟中断触发 CPU 调用调度器内核函数 。该函数暂停当前运行线程,保存寄存器信息到内存,选择下一个运行线程,恢复其注册表信息后继续执行 。此过程涉及完整上下文切换,包括保存和恢复线程状态、更新调度器数据结构,因内存访问及 CPU 周期消耗,操作较慢 。

Go 运行时有自己的调度器,采用 m:n 调度技术(将 m 个 goroutine 复用 / 调度到 n 个 OS 线程 )。与内核调度器不同,Go 调度器不由硬件时钟定期触发,而是由特定 Go 语言结构触发 ,如 goroutine 调用time.Sleep、被通道阻塞或进行互斥量操作时,调度器将其设为休眠模式,转而运行其他 goroutine,直到可唤醒该 goroutine 。由于无需切换到内核语境,调度 goroutine 成本比调度线程低很多 。

GOMAXPROCS

Go 调度器通过GOMAXPROCS参数确定同时执行 Go 代码所需的 OS 线程数量 ,默认值为机器上的 CPU 数量 。例如在 8 核 CPU 机器上,调度器会将 Go 代码调度到 8 个 OS 线程上执行(它是 m:n 调度中的 n )。处于休眠、被通道阻塞的 goroutine 不占用线程,阻塞在 I/O 及系统调用或调用非 Go 语言函数的 goroutine 虽需独立 OS 线程,但该线程不计入GOMAXPROCS

for {go fmt.Print(0)fmt.Print(1)
}
// $ GOMAXPROC=1 go run hacker-cliche.go  11111111111111111118008000000000000001111...
// $ GOMAXPROCS=2 go run hacker-cliche.go 01010101010101010101100110010101101001010...

可通过GOMAXPROCS环境变量或runtime.GOMAXPROCS函数显式控制该参数 。文中通过一个不断输出 0 和 1 的小程序示例展示其效果 ,当GOMAXPROCS=1时,每次最多一个 goroutine 运行,主 goroutine 和输出 0 的 goroutine 交替执行;当GOMAXPROCS=2时,两个 goroutine 可同时运行 。由于影响 goroutine 调度因素众多且运行时不断变化,实际结果可能不同。

goroutine没有标识

在多数支持多线程的操作系统和编程语言中,当前线程有独特标识,通常为整数或指针 。利用此标识可构建线程局部存储,即一个以线程标识为键的全局 map,使每个线程能独立存储和获取值,不受其他线程干扰 。

goroutine 没有可供程序员访问的标识 ,这是设计选择。因为线程局部存储易被滥用,如 Web 服务器使用支持线程局部存储的语言时,很多函数通过访问该存储查找 HTTP 请求信息,会导致类似过度依赖全局变量的 “超距作用”,使函数行为不仅取决于参数,还与运行线程标识有关,在需要改变线程标识(如使用工作线程 )时,函数行为会变得不可预测 。

Go 语言鼓励简单编程风格,函数行为应仅由显式指定参数决定,这样程序更易阅读,且在将函数子任务分发到多个 goroutine 时,无需考虑 goroutine 标识问题 。

参考资料:《Go程序设计语言》

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vue3 SSR Serverless架构革命:弹性计算与量子加速

一、全维度Serverless SSR架构 1.1 蜂巢式弹性调度系统 1.2 冷启动时间优化表 优化策略Node.js冷启(ms)Deno冷启(ms)Bun冷启(ms)裸启动1800960420预编译二进制650380210内存快照预热22016090WASM实例池15011075量子状态预载453832 二、边缘渲染协议升级 2.1 流式SSR响应协议…

FPAG IP核调用小练习

一、调用步骤 1、打开Quartus 右上角搜索ROM&#xff0c;如图所示 2、点击后会弹出如图所示 其中文件路径需要选择你自己的 3、点击OK弹出如图所示 图中红色改为12与1024 4、然后一直点NEXT&#xff0c;直到下图 这里要选择后缀为 .mif的文件 5、用C语言生成 .mif文件 //…

Spring Cloud 服务间调用深度解析

前言 在构建微服务架构时&#xff0c;服务间的高效通信是至关重要的。Spring Cloud 提供了一套完整的解决方案来实现服务间的调用、负载均衡、服务发现等功能。本文将深入探讨 Spring Cloud 中服务之间的调用机制&#xff0c;并通过源码片段和 Mermaid 图表帮助读者更好地理解…

AF3 generate_chain_data_cache脚本解读

AlphaFold3 generate_chain_data_cache 脚本在源代码的scripts文件夹下。该脚本从指定目录中批量解析 mmCIF/PDB 文件的工具,并将每个链的基本信息(序列、分辨率、是否属于聚类等)提取并写入 JSON 文件,主要用于后续蛋白质建模、过滤或训练数据准备。 源代码: import ar…

vue项目打包部署到maven仓库

需要的资源文件&#xff0c;都放在根目录下&#xff1a; 1. versionInfo.js const fs require(fs) const path require(path) const mkdirp require(mkdirp) const spawn require(child_process).spawnconst packageObj require(./package.json) const versionNo packa…

MegaTTS3: 下一代高效语音合成技术,重塑AI语音的自然与个性化

在近期的发布中&#xff0c;浙江大学赵洲教授团队与字节跳动联合推出了革命性的第三代语音合成模型——MegaTTS3&#xff0c;该模型不仅在多个专业评测中展现了卓越的性能&#xff0c;还为AI语音的自然性和个性化开辟了新的篇章。 MegaTTS3技术亮点 零样本语音合成 MegaTTS3采用…

【教程】PyTorch多机多卡分布式训练的参数说明 | 附通用启动脚本

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 目录 torchrun 一、什么是 torchrun 二、torchrun 的核心参数讲解 三、torchrun 会自动设置的环境变量 四、torchrun 启动过程举例 机器 A&#…

计算机视觉——基于 Yolov8 目标检测与 OpenCV 光流实现目标追踪

1. 概述 目标检测&#xff08;Object Detection&#xff09;和目标追踪&#xff08;Object Tracking&#xff09;是计算机视觉中的两个关键技术&#xff0c;它们在多种实际应用场景中发挥着重要作用。 目标检测指的是在静态图像或视频帧中识别出特定类别的目标对象&#xff0…

MySQL——流程控制

一、IF条件语句 语法 IF condition THENstatements; ELSEIF condition THENstatements; ELSEstatements; END IF; 判断成绩等级 # 判断成绩等级 # 输入学生的编号,取出学生的第一门课&#xff0c;然后判断当前的课程的等级 drop procedure if exists p2; delimiter $$ crea…

C# + Python混合开发实战:优势互补构建高效应用

文章目录 前言&#x1f94f;一、典型应用场景1. 桌面应用智能化2. 服务端性能优化3. 自动化运维工具 二、四大技术实现方案方案1&#xff1a;进程调用&#xff08;推荐指数&#xff1a;★★★★☆&#xff09;方案2&#xff1a;嵌入Python解释器&#xff08;推荐指数&#xff1…

MLflow 入门

官方主页 MLflow | MLflow官方文档 MLflow: A Tool for Managing the Machine Learning Lifecycle | MLflow 0. 简介 MLflow 是一个开源平台&#xff0c;专门为了帮助机器学习的从业者和团队处理机器学习过程中的复杂性而设计。MLflow 关注机器学习项目的完整生命周期&#x…

【蓝桥杯选拔赛真题101】Scratch吐丝的蜘蛛 第十五届蓝桥杯scratch图形化编程 少儿编程创意编程选拔赛真题解析

目录 scratch吐丝的蜘蛛 一、题目要求 1、准备工作 2、功能实现 二、案例分析 1、角色分析 2、背景分析 3、前期准备 三、解题思路 四、程序编写 五、考点分析 六、推荐资料 1、scratch资料 2、python资料 3、C++资料 scratch吐丝的蜘蛛 第十五届青少年蓝桥杯s…

智谱最新模型GLM4是如何练成的

写在前面 这篇博客将基于《ChatGLM: A Family of Large Language Models from GLM-130B to GLM-4 All Tools》,深入剖析 GLM-4 系列在**模型架构设计、预训练、后训练(对齐)、以及关键技术创新(如长上下文处理、Agent 能力构建)**等环节的实现逻辑与设计考量,带你全面了…

第二届电气技术与自动化工程国际学术会议 (ETAE 2025)

重要信息 2025年4月25-27日 中国广州 官网: http://www.icetae.com/ 部分 征稿主题 Track 1&#xff1a;电气工程 输配电、电磁兼容、高电压和绝缘技术、电气工程、电气测量、电力电子及其应用、机电一体化、电路与系统、电能质量和电磁兼容性、电力系统及其自…

设备调试--反思与总结

最近回顾项目&#xff0c; 发现&#xff1a;在调试过程中最耽误时间的可能不是技术难度&#xff0c;而是惯性思维&#xff1b; 例如&#xff1a; 我写can通信滤波器的时候&#xff0c;可能是不过滤的&#xff1b;是接收所有的id报文&#xff0c;然后用业务逻辑过滤&#xff08…

C++项目:高并发内存池_下

目录 8. thread cache回收内存 9. central cache回收内存 10. page cache回收内存 11. 大于256KB的内存申请和释放 11.1 申请 11.2 释放 12. 使用定长内存池脱离使用new 13. 释放对象时优化成不传对象大小 14. 多线程环境下对比malloc测试 15. 调试和复杂问题的调试技…

深度学习入门:神经网络的学习

目录 1 从数据中学习1.1 数据驱动1.2 训练数据和测试数据 2损失函数2.1 均方误差2.2 交叉熵误差2.3 mini-batch学习2.4 mini-batch版交叉熵误差的实现2.5 为何要设定损失函数 3 数值微分3.1 数值微分3.3 偏导数 4 梯度4.1 梯度法4.2 神经网络的梯度 5 学习算法的实现5.1 2层神经…

【第45节】windows程序的其他反调试手段上篇

目录 引言 一、通过窗口类名和窗口名判断 二、检测调试器进程 三、父进程是否是Explorer 四、RDTSC/GetTickCount时间敏感程序段 五、StartupInfo结构的使用 六、使用BeingDebugged字段 七、 PEB.NtGlobalFlag,Heap.HeapFlags,Heap.ForceFlags 八、DebugPort:CheckRem…

Golang|select

文章目录 多路监听超时控制 多路监听 如果selcet外面没有for循环&#xff0c;则只会监听一次&#xff0c;要实现一直监听的话要加for循环但是如果要设置退出条件的话&#xff0c;break语句只会退出这个select而不会退出for循环 select也可以有default&#xff0c;用于不用等cha…

无人机的群体协同与集群控制技术要点!

一、技术要点 通信技术 高效可靠的通信链路&#xff1a;无人机集群需要稳定、低延迟的通信网络&#xff0c;以实现实时数据传输和指令交互。通信方式包括无线自组织网络&#xff08;Ad Hoc&#xff09;、蜂窝网络、卫星通信等&#xff0c;需根据任务场景选择合适的通信技术。…