Go 1.9 sync.Map揭秘
http://colobu.com/2017/07/11/dive-into-sync-Map/
好详细,学习了
colobu 大师,你好
按照你这个文章, 我做了一个测试(代码贴在下面在了),有一些疑问:
- 为何
struct在不加锁的情况下并发读写,有WARNING: DATA RACE,但是不挂呢? - 那我们在开发服务端程序的时候,要如何"最佳实践"结构体的并发控制呢?
谢谢您了 :)
package main
import (
"sync"
)
func main() {
// concurrentMap()
concurrentStruct()
// concurrentStructWithMuLock()
}
type Metadata struct {
mu sync.RWMutex // 🔐
key bool
}
// concurrentStruct 并发操作结构体
// concurrent read and write the struct
// go run -race main.go 有 WARNING: DATA RACE,但是可以运行
// go run -race main.go It have WARNING: DATA RACE, But running ok
func concurrentStruct() {
m := new(Metadata)
for i := 0; i < 100000; i++ {
go func(metadata *Metadata) {
for {
readValue := metadata.key
if readValue {
metadata.key = false
}
}
}(m)
go func(metadata *Metadata) {
for {
metadata.key = true
}
}(m)
}
select {}
}
// concurrentStructWithMuLock 并发操作(使用了读写锁)结构体
// concurrent read and write the struct with RWMutex
// go run -race main.go 没有 WARNING: DATA RACE
// go run -race main.go Don't have WARNING: DATA RACE, and running ok
func concurrentStructWithMuLock() {
m := new(Metadata)
go func(metadata *Metadata) {
for {
metadata.mu.Lock()
readValue := metadata.key
if readValue {
metadata.key = false
}
metadata.mu.Unlock()
}
}(m)
go func(metadata *Metadata) {
for {
metadata.mu.Lock()
metadata.key = true
metadata.mu.Unlock()
}
}(m)
select {}
}
// concurrentMap 并发读写 Map
// concurrent read and write the map
// go run -race main.go 有 WARNING: DATA RACE,不可运行,fatal error: concurrent map read and map write
// go run -race main.go Have WARNING: DATA RACE, And fatal error: concurrent map read and map write
func concurrentMap() {
m := make(map[int]int)
go func() {
for {
_ = m[1]
}
}()
go func() {
for {
m[2] = 2
}
}()
select {}
}
因为metadata即使是检测到有并发读写,但是这个struct本身并没有在“出现并发读写时候”做额外的处理(panic),所以concurrentStruct不会出错。
与map对象相比,map类型会检测到, 比如 https://github.com/golang/go/blob/b080abf656feea5946922b2782bfeaa73cc317d4/src/runtime/map_fast64.go#L60,所以会panic。
如果你确定会有并发问题, 一般就使用concurrentStructWithMuLock
@fastcgi
colobu 大师,你好
按照你这个文章, 我做了一个测试(代码贴在下面在了),有一些疑问:
- 为何
struct在不加锁的情况下并发读写,有WARNING: DATA RACE,但是不挂呢?- 那我们在开发服务端程序的时候,要如何"最佳实践"结构体的并发控制呢?
谢谢您了 :)
@smallnest 因为
metadata即使是检测到有并发读写,但是这个struct本身并没有在“出现并发读写时候”做额外的处理(panic),所以concurrentStruct不会出错。与map对象相比,map类型会检测到, 比如 https://github.com/golang/go/blob/b080abf656feea5946922b2782bfeaa73cc317d4/src/runtime/map_fast64.go#L60,所以会panic。
如果你确定会有并发问题, 一般就使用
concurrentStructWithMuLock
如果想在 Metadata 上加一个 发现并发读写的功能要怎么做呢?
@fastcgi 如果想在 Metadata上加一个 发现并发读写的功能要怎么做呢?
多种方式,你可以参考map的实现。
或者设置一个 var flag uint32, 读写之前使用atomic CAS, 读写完重置为0。 如果CAS false则说明有其它goroutine正在读写它
@smallnest
多种方式,你可以参考map的实现。
或者设置一个
var flag uint32, 读写之前使用atomic CAS, 读写完重置为0。 如果CAS false则说明有其它goroutine正在读写它
感谢指点方向。 我晚上就试试。
空间换时间。 通过冗余的两个数据结构(read、dirty), "实现"加锁对性能的影响。
"降低"加锁对性能的影响
您好 非常想问您 您的博客前端是如何做出来的,我非常喜欢您的博客主题,如果是开源的 能告诉我连接吗?
我的理解:并发读写变量虽然有race,但没必要 panic,struct 其实也是变量
为什么 map 要 panic?我觉得主要是因为现在的扩容算法不支持并发。
@fastcgi
colobu 大师,你好
按照你这个文章, 我做了一个测试(代码贴在下面在了),有一些疑问:
- 为何
struct在不加锁的情况下并发读写,有WARNING: DATA RACE,但是不挂呢?- 那我们在开发服务端程序的时候,要如何"最佳实践"结构体的并发控制呢?
谢谢您了 :)
package main import ( "sync" ) func main() { // concurrentMap() concurrentStruct() // concurrentStructWithMuLock() } type Metadata struct { mu sync.RWMutex // 🔐 key bool } // concurrentStruct 并发操作结构体 // concurrent read and write the struct // go run -race main.go 有 WARNING: DATA RACE,但是可以运行 // go run -race main.go It have WARNING: DATA RACE, But running ok func concurrentStruct() { m := new(Metadata) for i := 0; i < 100000; i++ { go func(metadata *Metadata) { for { readValue := metadata.key if readValue { metadata.key = false } } }(m) go func(metadata *Metadata) { for { metadata.key = true } }(m) } select {} } // concurrentStructWithMuLock 并发操作(使用了读写锁)结构体 // concurrent read and write the struct with RWMutex // go run -race main.go 没有 WARNING: DATA RACE // go run -race main.go Don't have WARNING: DATA RACE, and running ok func concurrentStructWithMuLock() { m := new(Metadata) go func(metadata *Metadata) { for { metadata.mu.Lock() readValue := metadata.key if readValue { metadata.key = false } metadata.mu.Unlock() } }(m) go func(metadata *Metadata) { for { metadata.mu.Lock() metadata.key = true metadata.mu.Unlock() } }(m) select {} } // concurrentMap 并发读写 Map // concurrent read and write the map // go run -race main.go 有 WARNING: DATA RACE,不可运行,fatal error: concurrent map read and map write // go run -race main.go Have WARNING: DATA RACE, And fatal error: concurrent map read and map write func concurrentMap() { m := make(map[int]int) go func() { for { _ = m[1] } }() go func() { for { m[2] = 2 } }() select {} }
有一点没有看明白,还请大家帮忙解释一下:
当向map中插入新的之前不存在的记录时,会向dirty中写入这个记录,同时会将read中没有删除的记录拷贝到dirty中。因为后续当miss次数过多的时候,dirty会替换掉read。
但是在delete操作的时候,我发现只删除了read中的值,没有对dirty进行处理。这样如果后续进行miss切换的时候,之前删除掉的值不是就又出现了吗?
@GitHubSi 有一点没有看明白,还请大家帮忙解释一下:
当向map中插入新的之前不存在的记录时,会向dirty中写入这个记录,同时会将read中没有删除的记录拷贝到dirty中。因为后续当miss次数过多的时候,dirty会替换掉read。
但是在delete操作的时候,我发现只删除了read中的值,没有对dirty进行处理。这样如果后续进行miss切换的时候,之前删除掉的值不是就又出现了吗?
因为read和dirty指向同一个元素。 在read中标记了相当于在dirty中也标记了
func (e *entry) delete() (hadValue bool) {
for {
p := atomic.LoadPointer(&e.p)
// 已标记为删除
if p == nil || p == expunged {
return false
}
// 原子操作,e.p标记为nil
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return true
}
}
}
删除这里,如果在LoadPointer之后,&e.p被设置了新的值,这个值不是nil也不是expunged,那么在下一次for循环中,这个新的值不就被删除了吗?
不知道为啥,我在本机初始化数据后,执行单协程下100亿次读操作,耗时时间对比如下 RLocked map time: 5m12.799166666s map加读写锁,用读锁 unLocked map time: 5m7.516323292s map无锁 Locked map time: 5m19.848126958s map加写锁 sync.Map time: 6m49.719245417s sync.Map
测下来sync.Map 性能最差。后来又做了1000亿次的读操作,sync.Map花了1小时16分钟,只有sync.Map超过1小时了,其余都没操过1小时,大跌眼镜。。。。不知道哪里做的不对,请大佬指点
接上面的问题,测试代码如下:
import ( "fmt" "math/rand" "sync" "time" )
// SyncMapReader 用于读取 sync.Map type SyncMapReader struct { syncMap sync.Map }
func (sm *SyncMapReader) Read(key int) interface{} { val, _ := sm.syncMap.Load(key) return val }
// RLockedMapReader 用于读取带有读写锁的普通map type RLockedMapReader struct { mu sync.RWMutex mapVal map[int]interface{} }
func (lm *RLockedMapReader) Read(key int) interface{} { lm.mu.RLock() defer lm.mu.RUnlock() return lm.mapVal[key] }
// LockedMapReader 用于读取带有读写锁的普通map type LockedMapReader struct { mu sync.Mutex mapVal map[int]interface{} }
func (lm *LockedMapReader) Read(key int) interface{} { lm.mu.Lock() defer lm.mu.Unlock() return lm.mapVal[key] }
type MapReader struct { mapVal map[int]interface{} }
func testSyncMap() { sm := &SyncMapReader{} // 假设我们预先填充了sync.Map for i := 0; i < 100; i++ { sm.syncMap.Store(i, i) }
start := time.Now()
for i := 0; i < 10000000000; i++ {
_ = sm.Read(rand.Intn(100))
}
fmt.Println("sync.Map time:", time.Since(start))
}
func testRLockedMap() { lm := &RLockedMapReader{mapVal: make(map[int]interface{}, 100)} // 假设我们预先填充了带有读写锁的普通map for i := 0; i < 100; i++ { lm.mapVal[i] = i }
start := time.Now()
for i := 0; i < 10000000000; i++ {
_ = lm.Read(rand.Intn(100))
}
fmt.Println("RLocked map time:", time.Since(start))
}
func testLockedMap() { lm := &LockedMapReader{mapVal: make(map[int]interface{}, 100)} // 假设我们预先填充了带有读写锁的普通map for i := 0; i < 100; i++ { lm.mapVal[i] = i }
start := time.Now()
for i := 0; i < 10000000000; i++ {
_ = lm.Read(rand.Intn(100))
}
fmt.Println("Locked map time:", time.Since(start))
}
func testMap() { lm := &MapReader{mapVal: make(map[int]interface{}, 100)} // 假设我们预先填充了带有读写锁的普通map for i := 0; i < 100; i++ { lm.mapVal[i] = i }
start := time.Now()
for i := 0; i < 10000000000; i++ {
_ = lm.Read(rand.Intn(100))
}
fmt.Println("unLocked map time:", time.Since(start))
}
func (lm *MapReader) Read(key int) interface{} { return lm.mapVal[key] }
func main() { rand.Seed(time.Now().UnixNano()) fmt.Println("Running benchmarks...") testRLockedMap() testMap() testLockedMap() testSyncMap() }