首页
技术小册
AIGC
面试刷题
技术文章
MAGENTO
云计算
视频课程
源码下载
PDF书籍
「涨薪秘籍」
登录
注册
01|前世今生:你不得不了解的Go的历史和现状
02|拒绝“Hello and Bye”:Go语言的设计哲学是怎么一回事?
03|配好环境:选择一种最适合你的Go安装方法
04|初窥门径:一个Go程序的结构是怎样的?
05|标准先行:Go项目的布局标准是什么?
06|构建模式:Go是怎么解决包依赖管理问题的?
07|构建模式:Go Module的6类常规操作
08|入口函数与包初始化:搞清Go程序的执行次序
09|即学即练:构建一个Web服务就是这么简单
10|变量声明:静态语言有别于动态语言的重要特征
11|代码块与作用域:如何保证变量不会被遮蔽?
12|基本数据类型:Go原生支持的数值类型有哪些?
13|基本数据类型:为什么Go要原生支持字符串类型?
14|常量:Go在“常量”设计上的创新有哪些?
15|同构复合类型:从定长数组到变长切片
16|复合数据类型:原生map类型的实现机制是怎样的?
17|复合数据类型:用结构体建立对真实世界的抽象
18|控制结构:if的“快乐路径”原则
19|控制结构:Go的for循环,仅此一种
20|控制结构:Go中的switch语句有哪些变化?
21|函数:请叫我“一等公民”
22|函数:怎么结合多返回值进行错误处理?
23|函数:怎么让函数更简洁健壮?
24|方法:理解“方法”的本质
25|方法:方法集合与如何选择receiver类型?
26|方法:如何用类型嵌入模拟实现“继承”?
27|即学即练:跟踪函数调用链,理解代码更直观
28|接口:接口即契约
29|接口:为什么nil接口不等于nil?
30|接口:Go中最强大的魔法
31|并发:Go的并发方案实现方案是怎样的?
32|并发:聊聊Goroutine调度器的原理
33|并发:小channel中蕴含大智慧
34|并发:如何使用共享变量?
35|即学即练:如何实现一个轻量级线程池?
36|打稳根基:怎么实现一个TCP服务器?(上)
37|代码操练:怎么实现一个TCP服务器?(中)
38|成果优化:怎么实现一个TCP服务器?(下)
39 | 驯服泛型:了解类型参数
40|驯服泛型:定义泛型约束
41 | 驯服泛型:明确使用时机
当前位置:
首页>>
技术小册>>
Go语言入门实战经典
小册名称:Go语言入门实战经典
### 第34章 并发:如何使用共享变量? 在Go语言的并发编程中,共享变量的使用是一个核心而复杂的话题。Go通过goroutines和channels提供了强大的并发模型,但在多个goroutine之间安全地共享和修改数据却需要仔细设计。本章将深入探讨如何在Go中有效地使用共享变量,包括同步机制、原子操作以及内存模型的理解。 #### 34.1 并发与共享变量的挑战 并发编程的目的是提高程序的执行效率,通过同时运行多个任务来减少程序的整体运行时间。然而,当多个goroutine尝试同时访问或修改同一个变量时,就会出现数据竞争(Data Race)的问题。数据竞争是指两个或多个goroutine在没有适当同步的情况下,同时读写共享内存位置。这不仅可能导致数据不一致,还可能引发难以预测的程序行为,甚至崩溃。 #### 34.2 Go的内存模型与竞态条件 Go的内存模型定义了goroutines如何访问和修改共享内存。理解这一点对于编写安全的并发代码至关重要。Go内存模型保证: - 同一goroutine内的内存访问是顺序一致的。 - 初始的写入(在main函数开始之前)对所有goroutine可见。 - 对于两个goroutine之间的通信,通过channels或其他同步机制来保证内存访问的顺序性和可见性。 竞态条件(Race Condition)是指程序的结果依赖于并发执行的顺序或时序。在Go中,竞态条件常由数据竞争引起,但也可能由其他因素如I/O操作的时序差异导致。 #### 34.3 同步机制 为了避免数据竞争和竞态条件,Go提供了多种同步机制来确保多个goroutine在访问共享变量时的安全性。 ##### 34.3.1 互斥锁(Mutex) `sync.Mutex`是Go标准库`sync`包提供的一种互斥锁,用于保护临界区(Critical Section),即那些访问共享资源的代码段。使用互斥锁可以确保同一时刻只有一个goroutine能进入临界区。 ```go var ( mu sync.Mutex counter int ) func increment() { mu.Lock() // 临界区开始 counter++ // 临界区结束 mu.Unlock() } ``` ##### 34.3.2 读写锁(RWMutex) 对于读多写少的场景,使用`sync.RWMutex`比`sync.Mutex`更高效。`RWMutex`允许多个goroutine同时读取共享变量,但写入时仍需独占访问。 ```go var ( rwMu sync.RWMutex data int ) func readData() int { rwMu.RLock() defer rwMu.RUnlock() return data } func writeData(newValue int) { rwMu.Lock() defer rwMu.Unlock() data = newValue } ``` ##### 34.3.3 条件变量(Cond) `sync.Cond`用于在一个或多个goroutine之间协调操作,当满足特定条件时唤醒等待的goroutine。它通常与互斥锁一起使用,以避免竞态条件。 ```go var ( mu sync.Mutex cond *sync.Cond ready bool ) func init() { cond = sync.NewCond(&mu) } func waitForReady() { mu.Lock() for !ready { cond.Wait() } mu.Unlock() } func setReady() { mu.Lock() ready = true cond.Signal() mu.Unlock() } ``` #### 34.4 原子操作 对于简单的整数、布尔值或指针类型,Go的`sync/atomic`包提供了原子操作,这些操作在执行过程中不会被中断,从而保证了并发安全。 ```go import "sync/atomic" var counter int64 func increment() { atomic.AddInt64(&counter, 1) } func getCounter() int64 { return atomic.LoadInt64(&counter) } ``` 原子操作避免了使用互斥锁的开销,但仅限于对单个值的简单操作。 #### 34.5 使用Channels避免共享变量 在Go的并发模型中,channels是goroutines之间通信的主要方式。通过精心设计channels的使用,可以大大减少或避免直接对共享变量的依赖,从而降低数据竞争的风险。 ```go func counterGoroutine(ch chan<- int) { for i := 0; i < 100; i++ { ch <- 1 } close(ch) } func main() { ch := make(chan int) go counterGoroutine(ch) sum := 0 for value := range ch { sum += value } fmt.Println("Sum:", sum) } ``` 在这个例子中,我们使用了无缓冲的channel来传递计数值,而不是共享一个变量。每个goroutine都通过channel安全地发送和接收数据,从而避免了直接操作共享变量。 #### 34.6 总结与最佳实践 - **最小化共享变量的使用**:尽可能通过channels或其他机制避免共享变量。 - **使用同步机制**:对于必须共享的变量,使用互斥锁、读写锁或条件变量等同步机制来保护。 - **优先考虑原子操作**:对于简单的操作,考虑使用`sync/atomic`包提供的原子函数。 - **理解并遵守Go的内存模型**:确保对内存访问的顺序性和可见性有清晰的认识。 - **利用Go的并发特性**:通过goroutines和channels设计高效的并发程序,而不是依赖传统的线程同步机制。 通过遵循这些原则和最佳实践,你可以编写出既高效又安全的并发Go程序,充分利用Go语言的强大并发能力。
上一篇:
33|并发:小channel中蕴含大智慧
下一篇:
35|即学即练:如何实现一个轻量级线程池?
该分类下的相关小册推荐:
深入浅出Go语言核心编程(五)
深入浅出Go语言核心编程(三)
Go 组件设计与实现
Golang修炼指南
Golang并发编程实战
Go开发基础入门
深入浅出Go语言核心编程(一)
Go-Web编程实战
深入浅出Go语言核心编程(六)
深入浅出Go语言核心编程(七)
Go Web编程(上)
深入解析go语言