在Go语言中sync.Map和map的区别浅析

2023-06-01 00:00:00 语言 区别 浅析

在Go中,映射通常用于存储键值对,然而,当谈到并发编程时,我们需要小心访问共享数据以避免竞争条件。为了解决这个问题,Go 提供了sync.Map类型,它是 map 的一种安全并发的替代方案。

在本文中,我们将讨论sync.Map和Go 中的map之间的区别,以及如何在不同的场景中使用它们。


Go中的maps

map是一个无序的键值对的集合,其中的键是唯一的,值可以是任何类型。

map在Go中常用于存储数据,可以用make函数来创建,下面是代码:

m := make(map[string]int)

在这个例子中,我们创建了一个空的map,可以存储带有字符串键的整数值。我们可以使用方括号符号在map中添加或更新数值,正如下面的代码中提到的:

m["foo"] = 1
m["bar"] = 2

我们也可以使用方括号符号从map中检索数值,如下代码中提到的那样:

fmt.Println(m["foo"]) // 1

然而,map对于并发使用并不安全。如果多个goroutine试图同时访问或修改一个map,就会导致竞赛条件和数据损坏。为了避免这个问题,我们需要使用mutex或其他同步机制来确保每次只有一个goroutine可以访问map。


Go中的Sync.Map

sync.Map类型是Go中的一个内置类型,它提供了一个安全和并发的map替代品,它在go1.9版本中被引入。sync.Map类型使用与maps不同的内部实现,这使得它可以安全地并发使用,而不需要任何额外的同步。

创建sync.Map与创建map类似,只是我们不需要使用make函数:

var m sync.Map

在这个例子中,我们创建了一个空的sync.Map对象。

我们可以使用Store方法在map中添加或更新数值:

m.Store("foo", 1)
m.Store("bar", 2)

我们可以使用Load方法从map中检索数值:

if v, ok := m.Load("foo"); ok {
    fmt.Println(v) // 1
}

如果键存在的话,Load方法会返回与之相关的值,还有一个布尔值,表示该键在map中是否存在。

我们还可以使用Delete方法从map中删除值:

m.Delete("foo")

sync.Map类型还提供了一些其他的方法,比如Range,它允许我们遍历map中的所有键值对。Range方法以一个函数为参数,为map中的每个键值对调用该函数:

m.Range(func(key, value interface{}) bool {
    fmt.Printf("%v: %v\n", key, value)
    return true // continue iterating
})

Range函数返回true以继续迭代,或返回false以停止迭代。


sync.Map和maps之间的区别

Go中的sync.Map和maps有几个区别,如下所述:

1.安全性:

sync.Map对于并发使用是安全的,不需要任何额外的同步,而maps是不安全的,需要一个mutex或其他同步机制来避免竞赛条件。

2.初始化: 

map需要使用make函数进行初始化,而sync.Map则不需要初始化。

3.类型: 

Maps是静态类型,而sync.Map是动态类型,可以存储不同类型的值,不需要事先指定类型。

4.性能: 

在非并发使用中,Maps比sync.Map快,因为它们的开销更少。然而,当多个goroutines访问相同的数据时,sync.Map会更快,因为它消除了锁定和解锁突变的需要。

5.复制:

Maps是按值复制的,而sync.Map是按引用复制的。这意味着,如果我们把一个map传给一个函数,该函数会得到一个map的副本,而如果我们传给一个sync.Map,该函数会得到一个对原始map的引用。


什么时候使用sync.Map

sync.Map应该在我们需要在多个goroutine之间共享数据,并且不想使用mutex或其他同步机制时使用。sync.Map对于重读工作负载很有用,即多个goroutine频繁读取相同的数据,但只有少数goroutine在更新数据。

另一方面,如果我们有一个重写的工作负载,即多个goroutine频繁地更新数据,我们可能需要使用一个mutex或其他同步机制来确保每次只有一个goroutine可以访问数据。

在这种情况下,使用带有互斥器的map可能是一个更好的选择。


让我们看一下如何使用sync.Map的一些例子


例子1: 缓存

sync.Map的一个常见用例是缓存。在这个例子中,我们将创建一个简单的HTTP服务器,以ISO 8601格式返回当前时间。

我们将使用sync.Map来缓存每个请求的响应,这样我们就不需要为每个请求生成一个新的响应。

package main
import (
    "fmt"
    "log"
    "net/http"
    "sync"
    "time"
)
var cache sync.Map
func main() {
    http.HandleFunc("/", handler)
    log.Println(http.ListenAndServe(":8000", nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
    v, ok := cache.Load(r.URL.Path)
    if ok {
        fmt.Fprintln(w, v)
        return
    }
    response := time.Now().UTC().Format(time.RFC3339)
    cache.Store(r.URL.Path, response)
    fmt.Fprintln(w, response)
}

在这个例子中,我们创建了一个名为cache的sync.Map来存储每个请求的响应。

在处理函数中,我们首先使用Load方法检查当前请求的响应是否已经在缓存中。

如果是,我们就把响应写到客户端并提前返回。

否则,我们使用time.Now().UTC().Format(time.RFC3339)生成一个新的响应,使用Store方法将其存储在缓存中,然后写给客户端。


示例2:备忘录化

sync.Map的另一个常见用例是记忆化,这是一种缓存函数结果的技术,以避免对相同的输入进行重新计算。

在这个例子中,我们将创建一个简单的函数来计算第n个斐波那契数,并使用sync.Map来记忆结果以提高性能。

package main
import (
    "fmt"
    "sync"
)
var memo sync.Map
func main() {
    fmt.Println(fib(10))
    fmt.Println(fib(20))
}
func fib(n int) int {
    v, ok := memo.Load(n)
    if ok {
        return v.(int)
    }
    if n < 2 {
        memo.Store(n, n)
        return n
    }
    result := fib(n-1) + fib(n-2)
    memo.Store(n, result)
    return result
}

在这个例子中,我们创建了一个名为memo的sync.Map来存储以前的计算结果。

在fib函数中,我们首先使用Load方法检查当前输入的结果是否已经在memo中。

如果是,我们就提前返回结果。

否则,我们使用公式 fib(n-1) + fib(n-2) 递归计算结果,使用Store方法将其存储在memo中,并返回结果。


总结

总之,map和sync.Map都是Go中有用的数据结构,但它们有不同的使用情况和取舍。

map在非并发使用时速度快且简单,但在并发使用时需要像mutexes这样的同步机制。

sync.Map提供了内置的同步功能,对重读工作负载很有用,但它有一些限制,在非并发使用时比map慢。

通过了解这两种数据结构之间的差异和权衡,我们可以为我们的具体使用情况选择正确的数据结构,并优化我们的代码以提高性能和可扩展性。

相关文章