📚 原创系列: “Go语言学习系列”
🔄 转载说明: 本文最初发布于"Gopher部落"微信公众号,经原作者授权转载。
🔗 关注原创: 欢迎扫描文末二维码,关注"Gopher部落"微信公众号获取第一手Go技术文章。
📑 Go语言学习系列导航
🚀 第三阶段:进阶篇本文是【Go语言学习系列】的第33篇,当前位于第三阶段(进阶篇)
- 并发编程(一):goroutine基础
- 并发编程(二):channel基础
- 并发编程(三):select语句
- 并发编程(四):sync包
- 并发编程(五):并发模式
- 并发编程(六):原子操作与内存模型 👈 当前位置
- 数据库编程(一):SQL接口
- 数据库编程(二):ORM技术
- Web开发(一):路由与中间件
- Web开发(二):模板与静态资源
- Web开发(三):API开发
- Web开发(四):认证与授权
- Web开发(五):WebSocket
- 微服务(一):基础概念
- 微服务(二):gRPC入门
- 日志与监控
- 第三阶段项目实战:微服务聊天应用
📖 文章导读
在本文中,您将了解:
- 什么是原子操作,为什么它在并发编程中如此重要
- sync/atomic包提供的基本操作和新的原子类型
- Go内存模型中的happens-before关系及其重要性
- 内存重排和其对并发程序的影响
- 如何避免常见的并发陷阱和优化并发性能
- 使用原子操作实现高效的并发数据结构
并发编程(六):原子操作与内存模型
1. 什么是原子操作
在并发编程中,**原子操作(Atomic Operations)**是指不会被线程调度机制中断的操作,这类操作一旦开始,就会在CPU的一个时钟周期内执行完毕。原子操作在执行过程中不会被其他线程或进程打断,因此可以保证操作的完整性和一致性。
原子操作的主要特点:
- 不可分割性:原子操作是最小的执行单位,它要么完全执行,要么完全不执行
- 并发安全:多个线程同时对同一变量进行原子操作不会导致数据竞争
- 可见性保证:原子操作还确保操作的结果对其他线程可见
在Go语言中,原子操作是通过sync/atomic
包实现的,它提供了低级别的原子内存原语,用于实现同步算法。
2. 为什么需要原子操作
我们知道在并发编程中可以使用互斥锁(Mutex)来保护共享资源,那为什么还需要原子操作呢?以下是几个主要原因:
2.1 性能考虑
相比互斥锁,原子操作通常有更好的性能,因为它们:
- 不需要操作系统的调度器参与
- 直接在硬件层面实现
- 避免了上下文切换的开销
请看这个简单的性能对比:
package main
import (
"sync"
"sync/atomic"
"testing"
)
func BenchmarkMutex(b *testing.B) {
var count int64
var mu sync.Mutex
for i := 0; i < b.N; i++ {
mu.Lock()
count++
mu.Unlock()
}
}
func BenchmarkAtomic(b *testing.B) {
var count int64
for i := 0; i < b.N; i++ {
atomic.AddInt64(&count, 1)
}
}
在绝大多数情况下,原子操作的性能会比互斥锁高出数倍甚至数十倍。
2.2 适用于简单操作
原子操作非常适合于对单个变量进行简单操作,如:
- 递增/递减计数器
- 交换值
- 比较并交换(CAS)操作
2.3 避免死锁风险
使用原子操作可以避免某些死锁场景,因为它们不会阻塞线程。
3. sync/atomic包的基本操作
Go语言的sync/atomic
包提供了几类基本操作:
3.1 加载和存储操作
// 读取操作 - 原子地加载值
func Load(addr *T) T
// 存储操作 - 原子地存储值
func Store(addr *T, val T)
这些函数确保在多个goroutine之间读写变量时不会导致数据竞争。
示例:原子加载和存储
package main
import (
"fmt"
"sync/atomic"
"time"
)
func main() {
var value int32 = 0
// 并发写入
go func() {
for i := 0; i < 5; i++ {
atomic.StoreInt32(&value, int32(i))
time.Sleep(100 * time.Millisecond)
}
}()
// 并发读取
go func() {
for i := 0; i < 10; i++ {
val := atomic.LoadInt32(&value)
fmt.Println("Value:", val)
time.Sleep(50 * time.Millisecond)
}
}()
time.Sleep(1 * time.Second)
}
3.2 增减操作
// 增加操作 - 原子地将delta加到addr并返回新值
func Add(addr *T, delta T) T
// 减一操作 - 原子地将addr减1并返回新值
func AddInt64(addr *int64, delta int64) (new int64)
示例:原子计数器
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var counter int64 = 0
var wg sync.WaitGroup
// 启动100个goroutine,每个对计数器增加100
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
atomic.AddInt64(&counter, 1)
}
}()
}
wg.Wait()
fmt.Println("Final Counter:", counter) // 期望输出: 10000
}
3.3 交换操作
// 原子地将新值存入addr并返回旧值
func Swap(addr *T, new T) (old T)
示例:原子交换值
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var value int32 = 100
// 原子地将值从100交换成200,并获取旧值
oldValue := atomic.SwapInt32(&value, 200)
fmt.Println("Old value:", oldValue) // 输出: 100
fmt.Println("New value:", value) // 输出: 200
}
3.4 比较并交换(CAS)操作
// 如果addr的值等于old,则将new写入addr
func CompareAndSwap(addr *T, old, new T) (swapped bool)
CAS操作是实现无锁数据结构的基础,它允许我们在不使用互斥锁的情况下安全地修改值。
示例:使用CAS实现自旋锁
package main
import (
"fmt"
"sync"
"sync/atomic"
)
// 简单的自旋锁实现
type SpinLock struct {
locked int32
}
// 尝试获取锁
func (l *SpinLock) Lock() {
// 不断尝试将locked从0设为1
for !atomic.CompareAndSwapInt32(&l.locked, 0, 1) {
// 自旋等待
}
}
// 释放锁
func (l *SpinLock) Unlock() {
atomic.StoreInt32(&l.locked, 0)
}
func main() {
var counter int
var lock SpinLock
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
lock.Lock()
counter++
lock.Unlock()
}
}()
}
wg.Wait()
fmt.Println("Counter:", counter) // 应该等于10000
}
4. Go 1.19引入的新原子类型
Go 1.19版本对sync/atomic
包进行了重要升级,引入了一系列类型安全的原子类型:
atomic.Bool
- 用于原子布尔值操作atomic.Int64
/atomic.Int32
- 用于原子整数操作atomic.Uint64
/atomic.Uint32
- 用于原子无符号整数操作atomic.Pointer[T]
- 用于原子指针操作
这些新类型提供了更加类型安全和易用的API,推荐在新代码中使用。
4.1 使用atomic.Int64的示例
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
// 创建一个atomic.Int64值
var counter atomic.Int64
counter.Store(0) // 初始化为0
var wg sync.WaitGroup
// 启动10个goroutine,每个增加1000次
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
counter.Add(1)
}
}()
}
wg.Wait()
fmt.Println("Final counter:", counter.Load()) // 应该等于10000
}
4.2 使用atomic.Pointer的示例
package main
import (
"fmt"
"sync/atomic"
)
type Config struct {
MaxConnections int
Timeout int
}
func main() {
// 创建一个原子指针,指向Config结构体
var configPtr atomic.Pointer[Config]
// 初始化配置
initialConfig := &Config{MaxConnections: 10, Timeout: 30}
configPtr.Store(initialConfig)
// 读取当前配置
currentConfig := configPtr.Load()
fmt.Printf("Current config: %+v\n", currentConfig)
// 原子地更新配置
newConfig := &Config{MaxConnections: 20, Timeout: 60}
configPtr.Store(newConfig)
// 再次读取配置
updatedConfig := configPtr.Load()
fmt.Printf("Updated config: %+v\n", updatedConfig)
}
4.3 实现无锁数据结构
使用原子操作可以实现高性能的无锁数据结构。以下是一个简单的无锁队列实现示例:
package main
import (
"fmt"
"sync/atomic"
)
// 队列节点
type Node[T any] struct {
value T
next atomic.Pointer[Node[T]]
}
// 无锁队列
type LockFreeQueue[T any] struct {
head atomic.Pointer[Node[T]]
tail atomic.Pointer[Node[T]]
}
// 初始化队列
func NewLockFreeQueue[T any]() *LockFreeQueue[T] {
q := &LockFreeQueue[T]{}
// 创建哨兵节点
dummy := new(Node[T])
q.head.Store(dummy)
q.tail.Store(dummy)
return q
}
// 入队
func (q *LockFreeQueue[T]) Enqueue(value T) {
// 创建新节点
newNode := &Node[T]{value: value}
for {
tail := q.tail.Load()
next := tail.next.Load()
// 检查tail是否一致
if tail == q.tail.Load() {
if next == nil {
// 尝试添加新节点
if tail.next.CompareAndSwap(nil, newNode) {
// 更新尾指针
q.tail.CompareAndSwap(tail, newNode)
return
}
} else {
// 帮助更新尾指针
q.tail.CompareAndSwap(tail, next)
}
}
}
}
// 出队
func (q *LockFreeQueue[T]) Dequeue() (T, bool) {
var zero T
for {
head := q.head.Load()
tail := q.tail.Load()
next := head.next.Load()
// 检查head是否一致
if head == q.head.Load() {
// 队列为空
if head == tail {
if next == nil {
return zero, false
}
// 帮助更新尾指针
q.tail.CompareAndSwap(tail, next)
} else {
// 获取值并移动头指针
value := next.value
if q.head.CompareAndSwap(head, next) {
return value, true
}
}
}
}
}
func main() {
queue := NewLockFreeQueue[int]()
// 入队
for i := 1; i <= 5; i++ {
queue.Enqueue(i)
fmt.Printf("Enqueued: %d\n", i)
}
// 出队
for i := 0; i < 6; i++ {
if value, ok := queue.Dequeue(); ok {
fmt.Printf("Dequeued: %d\n", value)
} else {
fmt.Println("Queue is empty")
}
}
}
5. 原子操作的最佳实践与注意事项
5.1 最佳实践
-
优先使用新的类型安全API:在Go 1.19及以上版本中,优先使用新的类型安全API(如
atomic.Int64
而不是atomic.AddInt64
) -
对简单共享变量使用原子操作:对于简单的计数器、标志位等,使用原子操作比互斥锁更高效
-
使用原子操作构建更复杂的同步原语:如自旋锁、信号量等
5.2 注意事项
-
仅适用于单个变量:原子操作只适用于对单个变量的操作,不能原子地修改多个相关变量
-
避免过度使用:原子操作虽然高效,但过度使用会使代码难以理解和维护
-
内存排序的隐含语义:原子操作还隐含了特定的内存排序语义,这在使用CAS等高级操作时尤为重要
-
可能的ABA问题:在使用CAS操作时,需要注意ABA问题(值从A变为B又变回A,导致CAS无法检测变化)
6. Go内存模型的基础概念
在深入探讨Go的内存模型之前,我们需要了解什么是内存模型。内存模型定义了多线程程序中共享变量的可见性规则,它规定了在什么条件下,一个goroutine对变量的写入对另一个goroutine可见。
6.1 可见性与内存重排
在现代计算机架构中,为了提高性能,编译器和CPU可能会对指令进行重排序,这可能导致一个goroutine看到的变量状态与程序的书写顺序不一致。同时,每个CPU核心都有自己的缓存,一个核心对变量的修改可能不会立即反映到其他核心的缓存中。
这些情况会引起两个问题:
- 可见性问题:一个goroutine对变量的修改不一定对其他goroutine可见
- 重排序问题:指令可能按不同于程序中的顺序执行
6.2 Happens-Before关系
Go内存模型使用happens-before关系来定义内存操作的顺序。如果事件A happens-before事件B,那么A的效果(包括内存写入)对B是可见的。
Go语言规范中定义的几个重要的happens-before关系:
单个goroutine内的happens-before
在一个goroutine内部,程序的执行顺序和它的书写顺序一致(虽然编译器可能会重排指令,但必须保证结果与顺序执行相同)。
package main
import "fmt"
func main() {
a := 1
b := 2
a = 3 // 这发生在b = 4之前
b = 4
fmt.Println(a, b) // 必然打印 3 4
}
goroutine的创建与执行
启动goroutine的go语句happens-before该goroutine的执行开始。
package main
import (
"fmt"
"time"
)
func main() {
var a string
a = "hello" // 这happens-before下面的go语句
go func() {
fmt.Println(a) // 一定能看到"hello"
}()
time.Sleep(100 * time.Millisecond)
}
channel操作
Channel操作在Go内存模型中扮演着至关重要的角色,它们建立了跨goroutine的happens-before关系:
- 发送happens-before对应的接收完成:向channel发送的数据happens-before接收方从channel接收完成
- 关闭happens-before接收到关闭通知:关闭channel happens-before从channel接收到关闭通知(接收到零值)
- 无缓冲channel的接收happens-before发送完成:对于无缓冲channel,接收操作happens-before发送操作完成
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
x := 42
ch <- x // 发送x happens-before接收完成
}()
y := <-ch // 接收完成
fmt.Println(y) // 一定会打印42
}
使用缓冲channel控制顺序
有缓冲的channel可以用来控制操作的执行顺序:
package main
import "fmt"
func main() {
done := make(chan bool, 1)
// goroutine A
go func() {
fmt.Println("Hello") // 操作A
done <- true // 发送信号
}()
<-done // 等待信号
fmt.Println("World") // 操作B
}
在这个例子中,通过channel操作,我们确保"Hello"一定在"World"之前打印。
锁操作之间的happens-before
- 对于sync.Mutex或sync.RWMutex,Unlock操作happens-before后续的Lock操作
- 对于读写锁,RUnlock happens-before后续的Lock操作
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
var data int
go func() {
mu.Lock()
data = 42 // 写入数据
mu.Unlock() // 这happens-before后续的Lock操作
}()
// 其他goroutine
go func() {
mu.Lock() // 这happens-after前面的Unlock
fmt.Println(data) // 一定能看到42
mu.Unlock()
}()
}
WaitGroup操作之间的happens-before
sync.WaitGroup的Done操作happens-before Wait操作返回。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
var data int
wg.Add(1)
go func() {
defer wg.Done() // 这happens-before wg.Wait()返回
data = 42
}()
wg.Wait() // 等待goroutine完成
fmt.Println(data) // 一定能看到42
}
Once操作之间的happens-before
sync.Once的Do操作happens-before任何后续Do调用返回。
package main
import (
"fmt"
"sync"
)
func main() {
var once sync.Once
var data int
// 初始化函数
initFunc := func() {
data = 42
}
go func() {
once.Do(initFunc) // 可能会运行初始化函数
}()
go func() {
once.Do(initFunc) // 不会运行初始化函数
fmt.Println(data) // 一定能看到42
}()
}
原子操作之间的happens-before
原子操作(sync/atomic包中的操作)也建立了happens-before关系。例如,atomic.Store happens-before对相同变量的后续atomic.Load。
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var x atomic.Int32
go func() {
x.Store(42) // 这happens-before后续的Load操作
}()
go func() {
val := x.Load() // 如果这发生在Store之后,一定能看到42
fmt.Println(val)
}()
}
7. 内存重排序及其影响
内存重排序是现代计算机架构中提高性能的一种技术,但它会对并发程序产生重要影响。
7.1 编译器重排序
编译器可能会重排指令以提高性能,只要不改变单线程程序的行为。
// 原代码
x = 1
y = 2
// 可能被重排为
y = 2
x = 1
对于单线程程序,这种重排没有影响。但在多线程环境中,如果没有适当的同步,可能导致另一个goroutine观察到y已更新但x未更新的中间状态。
7.2 CPU重排序
CPU也可能重排指令,或者由于缓存一致性协议,使得内存操作的效果看起来是重排序的。
package main
func main() {
var a, b int
go func() {
a = 1
b = 2
}()
go func() {
// 下面的条件在理论上是可能成立的
// 由于CPU重排序或缓存不一致
if b == 2 && a == 0 {
// 这里的代码可能被执行
}
}()
}
7.3 内存栅栏(Memory Barriers)
为了控制内存重排序,CPU提供了内存栅栏指令。Go的同步原语(如互斥锁、channel和原子操作)隐式地使用这些指令来确保正确的内存顺序。
Go程序员通常不需要直接使用内存栅栏,因为标准库中的同步原语已经处理了这些底层细节。
8. 常见的并发陷阱与解决方法
8.1 数据竞争(Data Race)
数据竞争是并发编程中最常见的错误之一,它发生在两个goroutine同时访问同一块内存,且至少有一个是写操作时。
package main
import (
"fmt"
"time"
)
func main() {
counter := 0
// goroutine 1
go func() {
counter++ // 写操作
}()
// goroutine 2
go func() {
fmt.Println(counter) // 读操作
}()
time.Sleep(100 * time.Millisecond)
}
解决方法:
- 使用互斥锁
package main
import (
"fmt"
"sync"
"time"
)
func main() {
counter := 0
var mu sync.Mutex
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
go func() {
mu.Lock()
fmt.Println(counter)
mu.Unlock()
}()
time.Sleep(100 * time.Millisecond)
}
- 使用原子操作
package main
import (
"fmt"
"sync/atomic"
"time"
)
func main() {
var counter atomic.Int64
go func() {
counter.Add(1)
}()
go func() {
fmt.Println(counter.Load())
}()
time.Sleep(100 * time.Millisecond)
}
- 使用channel通信
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 1)
ch <- 0 // 初始值
go func() {
val := <-ch
val++
ch <- val
}()
go func() {
val := <-ch
fmt.Println(val)
ch <- val
}()
time.Sleep(100 * time.Millisecond)
}
8.2 内存可见性问题
即使没有数据竞争,由于缓存和重排序,一个goroutine对变量的修改可能不会立即对其他goroutine可见。
package main
import (
"fmt"
"time"
)
func main() {
stop := false
go func() {
for !stop {
// 一直循环
}
fmt.Println("Worker stopped")
}()
time.Sleep(time.Second)
stop = true // 这个修改可能不会被worker goroutine看到
time.Sleep(time.Second)
fmt.Println("Main exiting")
}
在某些情况下,上面的程序可能会一直运行而不会输出"Worker stopped",因为worker goroutine可能永远看不到stop变量的更新。
解决方法:
- 使用原子操作
package main
import (
"fmt"
"sync/atomic"
"time"
)
func main() {
var stop atomic.Bool
go func() {
for !stop.Load() {
// 一直循环
}
fmt.Println("Worker stopped")
}()
time.Sleep(time.Second)
stop.Store(true) // 这个修改一定会被worker goroutine看到
time.Sleep(time.Second)
fmt.Println("Main exiting")
}
- 使用channel通信
package main
import (
"fmt"
"time"
)
func main() {
stop := make(chan struct{})
go func() {
select {
case <-stop:
fmt.Println("Worker stopped")
return
default:
// 继续工作
}
}()
time.Sleep(time.Second)
close(stop) // 发送停止信号
time.Sleep(time.Second)
fmt.Println("Main exiting")
}
8.3 并发安全的单例模式
单例模式在多线程环境中需要特别注意。下面是一个并发安全的单例实现:
package main
import (
"fmt"
"sync"
)
type Singleton struct {
data string
}
var (
instance *Singleton
once sync.Once
)
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{data: "initialized"}
})
return instance
}
func main() {
var wg sync.WaitGroup
// 并发获取实例
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
s := GetInstance()
fmt.Printf("Instance: %p, Data: %s\n", s, s.data)
}()
}
wg.Wait()
}
使用sync.Once
保证初始化函数只执行一次,即使在并发环境下也是如此。
8.4 死锁与活锁
在使用互斥锁和channel时,需要避免死锁和活锁:
- 死锁:所有goroutine都在等待某个永远不会发生的事件
package main
func main() {
ch := make(chan int) // 无缓冲channel
ch <- 1 // 死锁:没有接收者,发送方会一直阻塞
<-ch
}
- 活锁:goroutine持续忙碌但无法推进
package main
import (
"fmt"
"sync/atomic"
"time"
)
func main() {
var a, b atomic.Int64
go func() {
for {
// 尝试将a从0改为1,但只有当b为0时
if a.Load() == 0 && b.Load() == 0 {
if a.CompareAndSwap(0, 1) {
fmt.Println("A changed to 1")
}
} else {
// 重置
a.Store(0)
}
}
}()
go func() {
for {
// 尝试将b从0改为1,但只有当a为0时
if a.Load() == 0 && b.Load() == 0 {
if b.CompareAndSwap(0, 1) {
fmt.Println("B changed to 1")
}
} else {
// 重置
b.Store(0)
}
}
}()
time.Sleep(time.Second)
}
解决方法:
-
对于死锁:
- 确保每个发送操作都有相应的接收操作
- 避免循环依赖锁
- 使用select和超时机制避免永久阻塞
-
对于活锁:
- 引入随机化或退避策略
- 优先级分配
- 减少竞争条件
package main
import (
"fmt"
"math/rand"
"sync/atomic"
"time"
)
func main() {
var a, b atomic.Int64
go func() {
for {
// 引入随机延迟
time.Sleep(time.Duration(rand.Intn(10)) * time.Millisecond)
if a.Load() == 0 && b.Load() == 0 {
if a.CompareAndSwap(0, 1) {
fmt.Println("A changed to 1")
time.Sleep(100 * time.Millisecond)
a.Store(0)
}
}
}
}()
go func() {
for {
// 引入随机延迟
time.Sleep(time.Duration(rand.Intn(10)) * time.Millisecond)
if a.Load() == 0 && b.Load() == 0 {
if b.CompareAndSwap(0, 1) {
fmt.Println("B changed to 1")
time.Sleep(100 * time.Millisecond)
b.Store(0)
}
}
}
}()
time.Sleep(time.Second)
}
👨💻 关于作者与Gopher部落
"Gopher部落"专注于Go语言技术分享,提供从入门到精通的完整学习路线。
🌟 为什么关注我们?
- 系统化学习路径:本系列44篇文章循序渐进,带你完整掌握Go开发
- 实战驱动教学:理论结合实践,每篇文章都有可操作的代码示例
- 持续更新内容:定期分享最新Go生态技术动态与大厂实践经验
- 专业技术社区:加入我们的技术交流群,与众多Go开发者共同成长
📱 关注方式
- 微信公众号:搜索 “Gopher部落” 或 “GopherTribe”
- 优快云专栏:点击页面右上角"关注"按钮
💡 读者福利
关注公众号回复 “Go并发” 即可获取:
- 完整Go并发编程示例代码
- Go并发模式实战指南PDF
- 高性能Go应用优化技巧
期待与您在Go语言的学习旅程中共同成长!