不使用锁处理并发访问map的方法
- sync.Map:
- Go语言标准库提供了
sync.Map
,它是一个线程安全的键值对集合。适合高并发场景下的读写操作。
- 示例代码:
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("key%d", id)
m.Store(key, id)
value, ok := m.Load(key)
if ok {
fmt.Printf("goroutine %d read value: %d\n", id, value)
}
}(i)
}
wg.Wait()
}
- 使用
channel
:
- 可以通过
channel
来封装对map的操作,所有对map的读写都通过channel
进行,这样可以避免直接在多个goroutine中操作map。
- 示例代码:
package main
import (
"fmt"
"sync"
)
type MapOp struct {
key string
value int
op string
reply chan interface{}
}
func mapHandler(m map[string]int, ops <-chan MapOp) {
for op := range ops {
switch op.op {
case "store":
m[op.key] = op.value
op.reply <- nil
case "load":
value, ok := m[op.key]
if ok {
op.reply <- value
} else {
op.reply <- nil
}
}
}
}
func main() {
var wg sync.WaitGroup
m := make(map[string]int)
ops := make(chan MapOp)
go mapHandler(m, ops)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("key%d", id)
reply := make(chan interface{})
ops <- MapOp{key: key, value: id, op: "store", reply: reply}
<-reply
close(reply)
reply = make(chan interface{})
ops <- MapOp{key: key, op: "load", reply: reply}
value := <-reply
if value != nil {
fmt.Printf("goroutine %d read value: %d\n", id, value)
}
close(reply)
}(i)
}
close(ops)
wg.Wait()
}
map默认不是线程安全的原因
- 设计理念:Go语言的设计理念强调简洁和高效。如果map默认是线程安全的,会增加map的实现复杂度,降低性能。因为线程安全的实现通常需要引入锁或其他同步机制,这会增加额外的开销。
- 通用性:不同的应用场景对并发控制的需求不同。如果map默认线程安全,就无法满足那些需要更细粒度并发控制或者希望自己实现更高效并发策略的场景。
这种设计选择的优点
- 高性能:没有内置的同步机制,使得map在单goroutine环境下性能更高。因为不需要额外的锁操作等开销,在非并发场景下可以更高效地进行读写。
- 灵活性:开发者可以根据具体的应用场景选择最合适的并发控制策略。比如在一些读多写少的场景下,可以使用读写锁(
sync.RWMutex
)来实现更高效的并发控制;而在一些极端高并发场景下,可以使用sync.Map
。
这种设计选择的缺点
- 容易出错:在并发场景下,如果开发者忘记对map进行适当的同步控制,就容易出现数据竞争问题,导致程序出现难以调试的错误。
- 增加开发成本:开发者需要花费额外的精力来处理map的并发访问,增加了开发和维护的成本。