gitalk icon indicating copy to clipboard operation
gitalk copied to clipboard

Go 1.9 sync.Map揭秘

Open smallnest opened this issue 8 years ago • 14 comments

http://colobu.com/2017/07/11/dive-into-sync-Map/

smallnest avatar Dec 27 '17 09:12 smallnest

好详细,学习了

zycfcn avatar Jan 19 '18 03:01 zycfcn

colobu 大师,你好

按照你这个文章, 我做了一个测试(代码贴在下面在了),有一些疑问:

  • 为何 struct在不加锁的情况下并发读写,有WARNING: DATA RACE,但是不挂呢?
  • 那我们在开发服务端程序的时候,要如何"最佳实践"结构体的并发控制呢?

附上 Stack Overflow 提问链接

谢谢您了 :)

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 {}
}

fastcgi avatar Jul 12 '18 09:07 fastcgi

因为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,但是不挂呢?
  • 那我们在开发服务端程序的时候,要如何"最佳实践"结构体的并发控制呢?

附上 Stack Overflow 提问链接

谢谢您了 :)

smallnest avatar Jul 12 '18 09:07 smallnest

@smallnest 因为metadata即使是检测到有并发读写,但是这个struct本身并没有在“出现并发读写时候”做额外的处理(panic),所以concurrentStruct不会出错。

与map对象相比,map类型会检测到, 比如 https://github.com/golang/go/blob/b080abf656feea5946922b2782bfeaa73cc317d4/src/runtime/map_fast64.go#L60,所以会panic。

如果你确定会有并发问题, 一般就使用concurrentStructWithMuLock

如果想在 Metadata 上加一个 发现并发读写的功能要怎么做呢?

fastcgi avatar Jul 12 '18 09:07 fastcgi

@fastcgi 如果想在 Metadata上加一个 发现并发读写的功能要怎么做呢?

多种方式,你可以参考map的实现。

或者设置一个 var flag uint32, 读写之前使用atomic CAS, 读写完重置为0。 如果CAS false则说明有其它goroutine正在读写它

smallnest avatar Jul 12 '18 10:07 smallnest

@smallnest

多种方式,你可以参考map的实现。

或者设置一个 var flag uint32, 读写之前使用atomic CAS, 读写完重置为0。 如果CAS false则说明有其它goroutine正在读写它

感谢指点方向。 我晚上就试试。

fastcgi avatar Jul 12 '18 11:07 fastcgi

空间换时间。 通过冗余的两个数据结构(read、dirty), "实现"加锁对性能的影响。

"降低"加锁对性能的影响

michelia avatar Apr 04 '19 02:04 michelia

您好 非常想问您 您的博客前端是如何做出来的,我非常喜欢您的博客主题,如果是开源的 能告诉我连接吗?

FelixHolmes avatar May 22 '19 10:05 FelixHolmes

我的理解:并发读写变量虽然有race,但没必要 panic,struct 其实也是变量

为什么 map 要 panic?我觉得主要是因为现在的扩容算法不支持并发。

@fastcgi

colobu 大师,你好

按照你这个文章, 我做了一个测试(代码贴在下面在了),有一些疑问:

  • 为何 struct在不加锁的情况下并发读写,有WARNING: DATA RACE,但是不挂呢?
  • 那我们在开发服务端程序的时候,要如何"最佳实践"结构体的并发控制呢?

附上 Stack Overflow 提问链接

谢谢您了 :)

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 {}
}

micln avatar Oct 07 '19 14:10 micln

有一点没有看明白,还请大家帮忙解释一下:

当向map中插入新的之前不存在的记录时,会向dirty中写入这个记录,同时会将read中没有删除的记录拷贝到dirty中。因为后续当miss次数过多的时候,dirty会替换掉read。

但是在delete操作的时候,我发现只删除了read中的值,没有对dirty进行处理。这样如果后续进行miss切换的时候,之前删除掉的值不是就又出现了吗?

think-next avatar Dec 15 '19 15:12 think-next

@GitHubSi 有一点没有看明白,还请大家帮忙解释一下:

当向map中插入新的之前不存在的记录时,会向dirty中写入这个记录,同时会将read中没有删除的记录拷贝到dirty中。因为后续当miss次数过多的时候,dirty会替换掉read。

但是在delete操作的时候,我发现只删除了read中的值,没有对dirty进行处理。这样如果后续进行miss切换的时候,之前删除掉的值不是就又出现了吗?

因为read和dirty指向同一个元素。 在read中标记了相当于在dirty中也标记了

smallnest avatar Dec 15 '19 15:12 smallnest

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循环中,这个新的值不就被删除了吗?

hheedat avatar May 14 '20 11:05 hheedat

不知道为啥,我在本机初始化数据后,执行单协程下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小时,大跌眼镜。。。。不知道哪里做的不对,请大佬指点

liyebing avatar Mar 23 '24 01:03 liyebing

接上面的问题,测试代码如下:

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() }

liyebing avatar Mar 23 '24 01:03 liyebing