如题,白白浪费我几天时间,偶发的并发bug不少,看我上篇文章就出现了各种bug,就不说了,现在来说个一定会出现的bug,
下面的代码在1.14.1版本是会报错的,结果不为1,但是我卸载后重新安装1.13.9之后就可以正常执行了!当然也可能是我的电脑原因,求大伙验证!
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
var m sync.Map
for i := 0; i < 65536; i++ {
m.Store(1, 1)
}
wg.Add(2)
go Add(m)
go sub(m)
wg.Wait()
fmt.Println(m.Load(1))
}
func Add(m sync.Map) {
for i := 0; i < 10000; i++ {
value, _ := m.Load(1)
v := value.(int)
v += 1
m.Store(1, v)
}
wg.Done()
}
func sub(m sync.Map) {
for i := 0; i < 10000; i++ {
value, _ := m.Load(1)
v := value.(int)
v -= 1
m.Store(1, v)
}
wg.Done()
}
不出意外的话,上面的额结果不会绝对是1 !!!大伙可以试试!我就不去PR了!
似乎1.13.9也会出现,可能真是我的电脑环境问题了!
有疑问加站长微信联系(非本文作者))

目测要被打脸?
你写的add和sub非线程(协程)安全的。 需要加锁或改成原子操作。
难道sync.map不就是为了解决并发安全问题?我没懂,求指教!
3楼 @anko sync.Map保证的并发安全是指Store和Load操作自身,而不是保证取到的值在其他操作时也是安全的。 Store操作包含了hash、查找、插入等等处理,这些操作无法在一个时钟周期内完成。 <br/>
<br/> 没有做并发安全的情况下,两个协程同时进行Store时, A协程在查找时没有,插入了一个值,B协程在查找时也没有值,但在插入覆盖了A插入的值。 <br/> <br/> 并发安全是保证了Store不会出现上面这种情况。同样Load是保证了不会读到正在Store而没有Store完成的值。 <br/> <br/> 你写的代码就是同样道理。不算v := value.(int)操作之外(不知道golang这个包含列哪些操作)。 Load这个是原子操作,v += 1分别包含了取值、加法计算、赋值三个操作, m.Store(1, v)是原子操作。 所以你的逻辑里起码有5个操作,只有保证这5个操作是原子的,才能算并发安全。
但是我用go run -race main.go 不会出现资源竞争的报错啊
其实sync.Map是指你对改map增、删、查是安全的,map操作需要在操作前上锁,保护内存,用sync.Map你不用上锁;这和两个线程同步没有关系,线程还是竞争关系
也就是,你的意思是,这种map并不保证读写互斥?(ー_ー)!!,仅仅是把锁的粒度缩小了而已?那么,我还需要给读写加上锁,那么,我现在的疑问是:对于并发安全需求,我用了这种map似乎更加损耗性能!!!!所以,你能举个它的应用场景吗?感谢
如果我还是要用到锁,那我感觉就没必要用到这种map了,我试了下,用了反而更加损耗性能!你能举个应用场景吗?最好有代码,谢谢!!
你说的报错是有panic异常吗?
不是,是结果不为1,是我对sync.map理解错误了!
我运行了很多次没有panic go version go1.14 windows/amd64
@zhengkeyu 我不是指报错,我要保证1加减1万次1还是1,不是panic,是线程安全,数据安全,线程同步
经过测试,如果在sync.map等等操作需要原子化的时候,还在外层加上锁来保证map的各种操作原子化的话,那么,他的速度仅仅是go原生的map(非sync.map)的性能的3分之一,不相信的朋友可以自己测试一个大数据就知道了,所以我很好奇设计sync.map的初衷是什么!!!我想不到他任何的应用场景!
5楼 @zhengkeyu 目前根据你的描述,在多个地方理解有问题。 建议你多了解下程序的原理。特别是各个数据类型的在内存中是如何存在的。 cpu是如何访问cpu缓存数据、以及内存数据的。 多核cpu如何保证在多线程下,保证内存中的数据一致的。 你说不会出现data race是因为你使用的是int。你分析下你的代码中哪些是分配在栈上,哪些是在堆中。 int传入sync.Map后是在内存中是如何存储的。当执行完你的逻辑后,变量和值都发生了什么变化。
给你改下就会出现data race了.
同时再给你一个不会出现data race的例子
经过测试,我发现如下性能比例:expvar.Map :加锁的map : sync.map = 0.75 : 1 : 3,
...第一个回复的前面一段话不是回复给你的。把你当发贴人了
看我回复zhengkeyu 的例子,把他当作你了。直接从中文意思浅层面的理解往往带来问题,还是得从程序原理上理解。 至于场景作用需要具体分析。 一个典型的场景是单个生产者,多消费者场景。
其实说白了,就是sync.map的锁粒度是局限于锁读和锁写,但是读和写可以同时进行,大概就是这个意思是吧?
我发现expvar.Map才是真正我需要的结构,就是map的值读写是原子性的!
非常感谢,不过真心求一个sync.map的应用场景!我新人不大懂!非常感谢2位大佬!敬上!
@anko 对于局限于锁读和锁写,不知道和你的是否一致。 感觉你的理解是 Store伪代码:
Load伪代码:
这么理解上,使用层面感觉不会有问题。但是建议还是从原理层面理解,更通用,任何语言都融会贯通
expvar.Map其实就是sync.Map的包装, 源码很少。理解我上面给的2个例子后,就知道了。
确实是sync.Map的包装,经过层层赛选,我已经将原本的6分钟优化到了1分3秒,最后还是抛弃了各种map,使用了数组和atomic.addint64()来做,我想分段计数应该还能优化到1分钟以下!非常感谢你的帮助!不是说map不好,而是我的业务不大适合!啊哈哈!
你这样改肯定就不行了啊 因为存的是指针 2个goroutine同时对一个内存地址进行写 之前他存的是值类型
例子一仅仅是对你说go run -race main.go没有data race而写的例子。用来比较为什么他原来的例子没有data race。
例子二不能算通用写法,但是对于发帖人的例子来说,是一种针对性的优化。 因为你会发现发的贴子中,其实都是key为1的值进行更改。map仅仅是作为仓库使用。所以根本不需要对map进行更新操作,只读操作就好了。
通过例二的优化,能减少map的处理。同时还减少对象,减少gc. 当然对比发帖人int这种值类型来说,反而增加列gc扫描成本。
load, store 是安全的,但load和store组合成的 add, sub 不安全,你这程序在任何语言中都不对的,不仅仅是Golang
28楼 @xxxcat 我主要是对sync.map不大熟悉,你看下expvar.Map,我以为他是expvar.Map,所以才会出现这种代码! expvar.Map是不用加锁的,内部已经加了,也就是读写互斥!他才是真的线程安全,而sync.map仅仅是操作原子性,但他并不是线程安全的!用了sync.map的地方很多都需要继续用锁!否则达不到用户级别的线程安全
sync及其子包主要解决了3个问题:线程安全,锁,原子操作。
首先,你的程序没有报错,只是数据与你预期不一致,说明并发没有问题。并发解决的是读的同时写的问题。你的代码压根就没有线程不安全
其次,你现在需要的是数据的递增递减功能,这是原子操作,也就是cas的范畴。具体的包是sync/atomic/ 这个包
线程安全和原子操作是完全不同的两个范畴。
你搞错了自己的需求,用错包了。
在觉得自己发现一个大bug之前,不妨先确认下,自己是不是用错了工具。
再说的更清楚写。
你的代码里达不到你预期的不是sync.map
而是+=
你需要用atomic 的add函数代替+=
我以为sync.map的功能类似expvar.Map,结果不是,所以这不全算是我的锅,整个网络没有几个人用sync.map写过线程并发的例子,看很多人都说sync.map是并发安全的,对,这个描述是不大准确的,如果真要说,只能说sync.map的各个api操作是并发安全的,但是,千万不要说sync.map这个对象或者类型是并发安全的!类似expvar.Map这个对象或者类型才是真的并发安全的!当然expvar.Map下面的所有操作都还是并发安全的!只要是设计到这个expvar.Map对象都会是并发安全的!其实本质是锁的粒度有大有小,expvar.Map刚好是大粒度锁,而sync.map只是锁住的是方法,而不是当前的expvar.Map对象,所以,我之所以会出现这种错误,恐怕跟我了解expvar.Map这个对象不多,目前网上大部分人都是研究sync.map对象而不是expvar.Map,因此我才会误将sync.map当做expvar.Map来使用了,至于atomic包下面的对象实现操作原子性,虽然不是锁,但是其实跟锁类似,比锁性能高点!所以我觉得go社区做的还不够!关于go的技术文章也经常是关于GC如何优化,G程,channel等等,不怎么全面覆盖到go的方方面面,比如我求一个sync.map的真实应用场景,恐怕应用场景不多!
33楼 @jarlyyn 就比如我自己测试过,我发现如下性能比例:expvar.Map :加锁的map : sync.map = 0.75 : 1 : 3,如果不是我去探究这个问题,恐怕go社区没个人知道的吧?文章标题确实是我很生气调试了2天都没能提高性能下写出的。我为此感到尴尬!不是我希望是博眼球,而是每次问个问题希望探讨一下都很少人来探讨!无奈之下的方法!请谅解!下次我不会这样取标题了!
sync.map的确是线程并发的,而且是并发安全的。
你对于并发安全和原子性的概念弄反了。
sync.map的典型场景是缓存/系统全局设置。每次设和取之前没有逻辑关系。
另外,sync.Map的性能测试和实现原理实际上网上很多。
而且sync.Map的代码注释写的非常清楚
// Map is like a Go map[interface{}]interface{} but is safe for concurrent use // by multiple goroutines without additional locking or coordination. // Loads, stores, and deletes run in amortized constant time. // // The Map type is specialized. Most code should use a plain Go map instead, // with separate locking or coordination, for better type safety and to make it // easier to maintain other invariants along with the map content. // // The Map type is optimized for two common use cases: (1) when the entry for a given // key is only ever written once but read many times, as in caches that only grow, // or (2) when multiple goroutines read, write, and overwrite entries for disjoint // sets of keys. In these two cases, use of a Map may significantly reduce lock // contention compared to a Go map paired with a separate Mutex or RWMutex.
大部分情况应该使用锁和map,sync.MAp适用两个场景
1.偶尔写入大量读取 2.多个进程会经常读写/覆盖不相关的节点的数据。
至于你说map加锁,只要看一下sync.Map的结构就知道,sync.map就是一个锁,一个 map,一个atomic.Value实现的,本来就是根据特殊场景规划好的锁+map
确实,官方说的很对,你说的也有道理,不过我们不冲突,我表达的是锁的粒度有大有小,而我的业务刚好既需要插入也需要更改,所以就不大适合这个sync.map,因为sync.map的粒度太小了,某些场合用它确实可以,如果经常需要修改的点,恐怕用这个sync.map就不适合了!
非常感谢,让我学到许多东西,不过关于原子,锁以及锁的粒度我应该没表达错误,
线程安全这点,你和anko理解上应该不同。 你说的并发安全是从数据内存访问这点出发的。Add和Sub方法由于访问的int是值类型,所以从内存访问上并不存在竞争问题。 而anko所说的并发安全应该是资源竞争,更倾向于方法的线程安全。从这个角度,写的Add和Sub是线程非安全的。
大佬发话了,是的,我的代码是要实现线程安全,我要保证几个api是原子性发生,而不是保证单个api线程安全!
sync.Map 与expvar.Map 面向的问题有所不同,前者是为了提供基础的线程安全容器,后者是要面向具体的需求,像C++、Java也有提供这类基础容器,它们都能保证容器本身的安全,但是都不保证容器中的元素并发安全。
容器的并发安全,容器中元素的并发安全,这二者不是一回事
所以,我个人觉得,就是不能说sync.map这个对象是线程安全的!应该说这个对象下的所有方法都是独立的线程安全的!但是他们组合在一起就不是线程安全的!不同的是,expvar.Map这个对象锁的粒度大点,他确实可以说是线程安全的!不关是组合这个对象下的所有方法,还是单个操作,都是线程安全的!这点跟sync.map是有挺大区别的!我确实在没发这个帖子之前我以为sync.map是类似于expvar.Map一样都是线程安全的,我没想到sync.map的锁粒度小点,只对单个的增删改查做了锁,但是当将增删改查混在一起写时候组合起来并不是线程安全的!所以感谢各位大佬了!学习中,让我们一起进步!
其实,锁分为2种,一种锁属性,一种锁对象,sync.map的那种就是将各个方法的执行强制限制为单个线程,而expvar.Map则是将这个对象的访问和修改统一限制为单个线程,sync.map锁住的是对象下的独立的每个方法,而expvar.Map锁住的是这个对象,自然就是比sync.map的锁粒度要大点!