Go 并发安全 Map 使用指南

Go 并发安全 Map 使用指南

本文旨在阐述在 go 语言并发环境下使用 Map 的正确姿势。重点讲解在读写并发的场景下,如何保证 Map 的数据安全,以及如何通过互斥锁(Mutex)来实现并发安全的 Map 访问。我们将通过示例代码和注意事项,帮助你更好地理解和应用并发安全的 Map。

并发 Map 的数据竞争问题

在 Go 语言中,内置的 map 类型并非线程安全。这意味着,如果在多个 goroutine 中同时读写同一个 map,可能会导致数据竞争,进而引发程序崩溃或其他未定义行为。

以下几种情况需要特别注意:

  • 多个读者,没有写者: 这种情况是安全的,可以并发读取。
  • 一个写者,没有读者: 这种情况也是安全的,写操作可以正常进行。
  • 至少一个写者,且至少一个读者或写者: 在这种情况下,所有读者和写者都必须使用同步机制来访问 map。

使用互斥锁(Mutex)实现并发安全 Map

最常用的方法是使用 sync.Mutex 来保护 map 的读写操作。 sync.RWMutex 提供了更细粒度的控制,允许并发读取,但仍然需要互斥写入。

使用 sync.Mutex

package main  import (     "fmt"     "sync"     "time" )  type ConcurrentMap struct {     sync.Mutex     data map[string]int }  func NewConcurrentMap() *ConcurrentMap {     return &ConcurrentMap{         data: make(map[string]int),     } }  func (m *ConcurrentMap) Set(key string, value int) {     m.Lock()     defer m.Unlock()     m.data[key] = value }  func (m *ConcurrentMap) Get(key string) (int, bool) {     m.Lock()     defer m.Unlock()     val, ok := m.data[key]     return val, ok }  func (m *ConcurrentMap) Delete(key string) {     m.Lock()     defer m.Unlock()     delete(m.data, key) }  func main() {     cmap := NewConcurrentMap()      var wg sync.WaitGroup      // 启动多个 goroutine 并发写入     for i := 0; i < 10; i++ {         wg.Add(1)         go func(i int) {             defer wg.Done()             key := fmt.Sprintf("key-%d", i)             cmap.Set(key, i*10)             time.Sleep(time.Millisecond * 10) // 模拟一些耗时操作         }(i)     }      // 启动多个 goroutine 并发读取     for i := 0; i < 5; i++ {         wg.Add(1)         go func(i int) {             defer wg.Done()             key := fmt.Sprintf("key-%d", i)             val, ok := cmap.Get(key)             if ok {                 fmt.Printf("Goroutine %d: key=%s, value=%dn", i, key, val)             } else {                 fmt.Printf("Goroutine %d: key=%s not foundn", i, key)             }             time.Sleep(time.Millisecond * 5) // 模拟一些耗时操作         }(i)     }      wg.Wait() // 等待所有 goroutine 完成     fmt.Println("Done.") }

代码解释:

Go 并发安全 Map 使用指南

Bardeen AI

使用AI自动执行人工任务

Go 并发安全 Map 使用指南59

查看详情 Go 并发安全 Map 使用指南

  1. ConcurrentMap 结构体包含一个 sync.Mutex 和一个 map[string]int。
  2. Set、Get 和 Delete 方法在访问 map 之前都会先获取锁,并在操作完成后释放锁,保证了并发安全。
  3. main 函数启动多个 goroutine 并发读写 ConcurrentMap,使用 sync.WaitGroup 等待所有 goroutine 完成。

使用 sync.RWMutex

package main  import (     "fmt"     "sync"     "time" )  type ConcurrentMap struct {     sync.RWMutex     data map[string]int }  func NewConcurrentMap() *ConcurrentMap {     return &ConcurrentMap{         data: make(map[string]int),     } }  func (m *ConcurrentMap) Set(key string, value int) {     m.Lock() // 使用写锁     defer m.Unlock()     m.data[key] = value }  func (m *ConcurrentMap) Get(key string) (int, bool) {     m.RLock() // 使用读锁     defer m.RUnlock()     val, ok := m.data[key]     return val, ok }  func (m *ConcurrentMap) Delete(key string) {     m.Lock() // 使用写锁     defer m.Unlock()     delete(m.data, key) }  func main() {     cmap := NewConcurrentMap()      var wg sync.WaitGroup      // 启动多个 goroutine 并发写入     for i := 0; i < 3; i++ {         wg.Add(1)         go func(i int) {             defer wg.Done()             key := fmt.Sprintf("key-%d", i)             cmap.Set(key, i*10)             time.Sleep(time.Millisecond * 10) // 模拟一些耗时操作         }(i)     }      // 启动多个 goroutine 并发读取     for i := 0; i < 5; i++ {         wg.Add(1)         go func(i int) {             defer wg.Done()             key := fmt.Sprintf("key-%d", i)             val, ok := cmap.Get(key)             if ok {                 fmt.Printf("Goroutine %d: key=%s, value=%dn", i, key, val)             } else {                 fmt.Printf("Goroutine %d: key=%s not foundn", i, key)             }             time.Sleep(time.Millisecond * 5) // 模拟一些耗时操作         }(i)     }      wg.Wait() // 等待所有 goroutine 完成     fmt.Println("Done.") }

代码解释:

Go 并发安全 Map 使用指南

Bardeen AI

使用AI自动执行人工任务

Go 并发安全 Map 使用指南59

查看详情 Go 并发安全 Map 使用指南

  1. 与使用 sync.Mutex 的例子类似,但使用了 sync.RWMutex。
  2. Get 方法使用 RLock() 获取读锁,允许并发读取。
  3. Set 和 Delete 方法仍然使用 Lock() 获取写锁,保证写操作的互斥性。

注意事项

  • 锁的粒度: 锁的粒度会影响程序的性能。 锁的范围越小,并发性越高,但也会增加锁管理的开销。
  • 死锁: 避免死锁的发生。 确保锁的获取顺序一致,并且在持有锁的时候避免调用其他需要获取锁的函数。
  • defer 释放锁: 使用 defer 语句来确保锁在函数退出时总是被释放,避免锁泄漏。
  • 性能考量: 虽然互斥锁可以保证并发安全,但也会带来性能损耗。 在高并发场景下,可以考虑使用更高级的并发控制技术,例如分片 map、原子操作等。

使用 sync.Map

Go 1.9 引入了 sync.Map 类型,它是一种并发安全的 map 实现,无需显式加锁。 sync.Map 内部使用了更复杂的机制来减少锁的竞争,在高并发场景下可能比使用 sync.Mutex 的 map 性能更好。

package main  import (     "fmt"     "sync"     "time" )  func main() {     var m sync.Map      var wg sync.WaitGroup      // 启动多个 goroutine 并发写入     for i := 0; i < 10; i++ {         wg.Add(1)         go func(i int) {             defer wg.Done()             key := fmt.Sprintf("key-%d", i)             m.Store(key, i*10)             time.Sleep(time.Millisecond * 10) // 模拟一些耗时操作         }(i)     }      // 启动多个 goroutine 并发读取     for i := 0; i < 5; i++ {         wg.Add(1)         go func(i int) {             defer wg.Done()             key := fmt.Sprintf("key-%d", i)             val, ok := m.Load(key)             if ok {                 fmt.Printf("Goroutine %d: key=%s, value=%vn", i, key, val)             } else {                 fmt.Printf("Goroutine %d: key=%s not foundn", i, key)             }             time.Sleep(time.Millisecond * 5) // 模拟一些耗时操作         }(i)     }      wg.Wait() // 等待所有 goroutine 完成     fmt.Println("Done.") }

代码解释:

Go 并发安全 Map 使用指南

Bardeen AI

使用AI自动执行人工任务

Go 并发安全 Map 使用指南59

查看详情 Go 并发安全 Map 使用指南

  1. 直接使用 sync.Map 类型,无需手动创建 map。
  2. 使用 Store 方法写入数据,使用 Load 方法读取数据。
  3. sync.Map 提供了 LoadOrStore、Delete、Range 等方法,可以根据实际需求选择使用。

sync.Map 的适用场景

  • 读多写少: sync.Map 在读多写少的场景下性能较好。
  • key 不频繁变化: sync.Map 针对 key 的增删改查操作做了优化,但如果 key 频繁变化,性能可能会下降。

总结

在 Go 语言并发编程中,确保 map 的并发安全至关重要。 可以使用 sync.Mutex 或 sync.RWMutex 来保护 map 的读写操作,也可以使用 Go 1.9 引入的 sync.Map 类型。 选择哪种方法取决于具体的应用场景和性能需求。 务必注意锁的粒度、死锁避免和性能考量,编写健壮且高效的并发程序。

go ai 并发编程 同步机制 有锁 String 子类 结构体 int 线程 map delete 并发

上一篇
下一篇