Golang协程中的读写安全性探讨

Golang协程中的读写安全性探讨 大家好

我编写了一个函数,用于读取一些数据,这些数据可能计算量很大,需要反复计算。因此,该函数在首次读取时将数据缓存到映射中,之后直接从映射返回值。

从多个 Go 协程中访问此函数是否安全?

2 回复

你好 @Michael_Hugi,

跨 goroutine 读取 map 是没有问题的。对于写入操作,你需要使用同步机制,因为 map 不是线程安全的。

确保对 map 的写入操作是线程安全的可能选项包括:

  • 使用 sync.RWMutex,如这个 Stack Overflow 回答所示。简而言之:读操作使用 RLock() 来阻止写入者,但不阻止其他读取者。写操作使用 Lock() 来阻止所有其他操作。
  • 使用 sync.Map,但请注意,由于使用了 interface{},你将失去编译时的类型安全。(1.18 或更高版本中的泛型将解决这个缺点。)

更多关于Golang协程中的读写安全性探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Go语言中,从多个协程并发访问一个缓存映射是不安全的,除非你采取适当的同步措施。你的函数需要保护对共享映射的读写操作,以避免数据竞争和未定义行为。

以下是一个使用sync.RWMutex实现线程安全缓存的示例:

package main

import (
    "sync"
)

type Cache struct {
    mu    sync.RWMutex
    items map[string]interface{}
}

func NewCache() *Cache {
    return &Cache{
        items: make(map[string]interface{}),
    }
}

func (c *Cache) Get(key string, computeFunc func() interface{}) interface{} {
    // 先尝试获取读锁
    c.mu.RLock()
    val, found := c.items[key]
    c.mu.RUnlock()
    
    if found {
        return val
    }
    
    // 未命中缓存,获取写锁
    c.mu.Lock()
    defer c.mu.Unlock()
    
    // 双重检查,防止其他协程已经写入
    if val, found := c.items[key]; found {
        return val
    }
    
    // 计算并缓存结果
    val = computeFunc()
    c.items[key] = val
    return val
}

// 使用示例
func main() {
    cache := NewCache()
    
    // 模拟多个协程并发访问
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            
            result := cache.Get("expensive_data", func() interface{} {
                // 模拟耗时计算
                return id * 100
            })
            _ = result // 使用结果
        }(i)
    }
    wg.Wait()
}

如果你使用的是Go 1.9+版本,也可以使用sync.Map来简化并发安全的映射操作:

package main

import (
    "sync"
)

type SyncMapCache struct {
    m sync.Map
}

func (c *SyncMapCache) Get(key string, computeFunc func() interface{}) interface{} {
    // 尝试直接加载
    if val, ok := c.m.Load(key); ok {
        return val
    }
    
    // 计算新值
    newVal := computeFunc()
    
    // 尝试存储,如果键已存在则返回现有值
    if val, loaded := c.m.LoadOrStore(key, newVal); loaded {
        return val
    }
    
    return newVal
}

// 使用示例
func main() {
    cache := &SyncMapCache{}
    
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            
            result := cache.Get("data", func() interface{} {
                return "computed_value"
            })
            _ = result
        }(i)
    }
    wg.Wait()
}

第一个示例使用读写锁,适合读多写少的场景;第二个示例使用sync.Map,更适合键值对数量较多或键空间较大的情况。两个实现都确保了多协程访问的安全性。

回到顶部