前言
线程安全通常是指在并发环境下,共享资源的访问被适当地管理,以防止竞争条件(race conditions)导致的数据不一致
Go语言中的线程安全可以通过多种方式实现
实现方式
- 互斥锁(Mutexes)
Go的sync包提供了Mutex和RWMutex类型来确保在一个时间点只有一个协程可以访问某个资源
go
import "sync"
var mu sync.Mutex
var sharedResource map[string]int
func updateResource(key string, value int) {
mu.Lock() // 加锁
sharedResource[key] = value
mu.Unlock() // 解锁
}
- 原子操作(Atomic operations)
sync/atomic包提供了一系列原子操作函数,可用于管理基本数据类型的并发访问
go
import "sync/atomic"
var count int64
func increment() {
atomic.AddInt64(&count, 1) // 原子地增加计数
}
- 通道(Channels)
通过使用通道,可以在协程之间安全地传递数据。当数据通过通道从一个协程传递到另一个协程时,不需要额外的同步机制
go
ch := make(chan int)
// 发送者
go func() {
ch <- 42
}()
// 接收者
go func() {
value := <-ch
fmt.Println(value)
}()
-
不可变性(Immutability)
不修改数据可以自然地避免并发问题。设计数据结构和算法时,尽可能使数据不可变,可以减少同步的需要
-
其他同步原语
sync包还提供了其他同步原语,如WaitGroup、Once、Cond等,可以用来同步协程的不同行为
使用上述任何一种机制时,都需要仔细设计代码以避免死锁、活锁或饥饿等问题。在Go中,可以使用go run -race
命令来检测代码中的竞争条件
sync.Map
sync.Map
是一个线程安全的映射(map),它是在 sync 包中提供的。与使用互斥锁来保护普通的 map 不同,sync.Map
使用了一种无锁的技术,特别适用于以下两种场景:
- 当给定键的条目只写入一次但读取多次时,比如在全局缓存中
- 当多个协程读取、写入和覆盖不相交的键集的条目时
sync.Map
提供了一些内置方法来操作线程安全的键值对:
Store(key, value
): 存储键值对Load(key)
: 根据键获取值LoadOrStore(key, value)
: 获取或存储键值对。如果键已经存在,则返回现有的键值对和 false;如果不存在,则存储并返回键值对和 trueDelete(key)
: 删除键值对Range(f func(key, value interface{}) bool)
: 遍历所有键值对,对每个键值对执行给定的函数 f
看下基本用法
go
import (
"fmt"
"sync"
)
func main() {
var sm sync.Map
// 存储键值对
sm.Store("hello", "world")
sm.Store(1, 3)
// 读取键对应的值
if value, ok := sm.Load("hello"); ok {
fmt.Println("hello:", value)
}
// 删除键
sm.Delete("hello")
// 遍历所有键值对
sm.Range(func(key, value interface{}) bool {
fmt.Println(key, value)
return true // 继续遍历
})
}
请注意,尽管 sync.Map 提供了线程安全的操作,但是它的性能通常会比使用互斥锁保护的普通 map 差,因此只推荐在上述特定场景中使用