Golang一文解决Map并发问题

在Go1.6之前,内置的map类型是部分goroutine安全的,并发的读没有问题,并发的写可能有问题。自go1.6之后,并发地读写map会报错,这在一些知名的开源库中都存在这个问题,所以go1.9之前的解决方案是额外绑定一个锁,封装成一个新的struct或者单独使用锁都可以。

本文带你深入到sync.Map的具体实现中,看看为了增加一个功能,代码是如何变的复杂的,以及作者在实现sync.Map的一些思想。

有并发问题的map

官方的faq已经提到内建的map不是线程(goroutine)安全的。

首先,让我们看一段并发读写的代码,下列程序中一个goroutine一直读,一个goroutine一只写同一个键值,即即使读写的键不相同,而且map也没有"扩容"等操作,代码还是会报错。

packagemainfuncmain(){  m:=make(map[int]int)  gofunc(){    for{      _=m[1]    }  }()  gofunc(){    for{      m[2]=2    }  }()  select{}}

错误信息是:fatalerror:concurrentmapreadandmapwrite。

如果你查看Go的源代码:hashmap_fast.go#L,会看到读的时候会检查hashWriting标志,如果有这个标志,就会报并发错误。

写的时候会设置这个标志:hashmap.go#L

h.flags

=hashWriting

hashmap.go#L设置完之后会取消这个标记。

当然,代码中还有好几处并发读写的检查,比如写的时候也会检查是不是有并发的写,删除键的时候类似写,遍历的时候并发读写问题等。

有时候,map的并发问题不是那么容易被发现,你可以利用-race参数来检查。

Go1.9之前的解决方案

但是,很多时候,我们会并发地使用map对象,尤其是在一定规模的项目中,map总会保存goroutine共享的数据。在Go官方blog的Gomapsinaction一文中,提供了一种简便的解决方案。

varcounter=struct{sync.RWMutexmmap[string]int}{m:make(map[string]int)}

它使用嵌入struct为map增加一个读写锁。

读数据的时候很方便的加锁:

counter.RLock()n:=counter.m["some_key"]counter.RUnlock()fmt.Println("some_key:",n)

写数据的时候:

counter.Lock()counter.m["some_key"]++counter.Unlock()sync.Map

可以说,上面的解决方案相当简洁,并且利用读写锁而不是Mutex可以进一步减少读写的时候因为锁带来的性能。

但是,它在一些场景下也有问题,如果熟悉Java的同学,可以对比一下java的ConcurrentHashMap的实现,在map的数据非常大的情况下,一把锁会导致大并发的客户端共争一把锁,Java的解决方案是shard,内部使用多个锁,每个区间共享一把锁,这样减少了数据共享一把锁带来的性能影响,orcaman提供了这个思路的一个实现:concurrent-map,他也询问了Go相关的开发人员是否在Go中也实现这种方案,由于实现的复杂性,答案是Yes,weconsideredit.,但是除非有特别的性能提升和应用场景,否则没有进一步的开发消息。

那么,在Go1.9中sync.Map是怎么实现的呢?它是如何解决并发提升性能的呢?

sync.Map的实现有几个优化点,这里先列出来,我们后面慢慢分析。

空间换时间。通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。使用只读数据(read),避免读写冲突。动态调整,miss次数多了之后,将dirty数据提升为read。double-checking。延迟删除。删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据。优先从read读取、更新、删除,因为对read的读取不需要锁。

下面我们介绍sync.Map的重点代码,以便理解它的实现思想。

首先,我们看一下sync.Map的数据结构:

typeMapstruct{  //当涉及到dirty数据的操作的时候,需要使用这个锁  muMutex  //一个只读的数据结构,因为只读,所以不会有读写冲突。  //所以从这个数据中读取总是安全的。  //实际上,实际也会更新这个数据的entries,如果entry是未删除的(unexpunged),并不需要加锁。如果entry已经被删除了,需要加锁,以便更新dirty数据。  readatomic.Value//readOnly  //dirty数据包含当前的map包含的entries,它包含最新的entries(包括read中未删除的数据,虽有冗余,但是提升dirty字段为read的时候非常快,不用一个一个的复制,而是直接将这个数据结构作为read字段的一部分),有些数据还可能没有移动到read字段中。  //对于dirty的操作需要加锁,因为对它的操作可能会有读写竞争。  //当dirty为空的时候,比如初始化或者刚提升完,下一次的写操作会复制read字段中未删除的数据到这个数据中。  dirtymap[interface{}]*entry  //当从Map中读取entry的时候,如果read中不包含这个entry,会尝试从dirty中读取,这个时候会将misses加一,  //当misses累积到dirty的长度的时候,就会将dirty提升为read,避免从dirty中miss太多次。因为操作dirty需要加锁。  missesint}

它的数据结构很简单,值包含四个字段:read、mu、dirty、misses。

它使用了冗余的数据结构read、dirty。dirty中会包含read中为删除的entries,新增加的entries会加入到dirty中。

read的数据结构是:

typereadOnlystruct{  mmap[interface{}]*entry  amendedbool//如果Map.dirty有些数据不在中的时候,这个值为true}

amended指明Map.dirty中有readOnly.m未包含的数据,所以如果从Map.read找不到数据的话,还要进一步到Map.dirty中查找。

对Map.read的修改是通过原子操作进行的。

虽然read和dirty有冗余数据,但这些数据是通过指针指向同一个数据,所以尽管Map的value会很大,但是冗余的空间占用还是有限的。

readOnly.m和Map.dirty存储的值类型是*entry,它包含一个指针p,指向用户存储的value值。

typeentrystruct{  punsafe.Pointer//*interface{}}

p有三种值:

nil:entry已被删除了,并且m.dirty为nilexpunged:entry已被删除了,并且m.dirty不为nil,而且这个entry不存在于m.dirty中其它:entry是一个正常的值

以上是sync.Map的数据结构,下面我们重点看看Load、Store、Delete、Range这四个方法,其它辅助方法可以参考这四个方法来理解。

Load

加载方法,也就是提供一个键key,查找对应的值value,如果不存在,通过ok反映:

func(m*Map)Load(keyinterface{})(valueinterface{},okbool){  //1.首先从m.read中得到只读readOnly,从它的map中查找,不需要加锁  read,_:=m.read.Load().(readOnly)  e,ok:=read.m[key]  //2.如果没找到,并且m.dirty中有新数据,需要从m.dirty查找,这个时候需要加锁  if!okread.amended{    m.mu.Lock()    //双检查,避免加锁的时候m.dirty提升为m.read,这个时候m.read可能被替换了。    read,_=m.read.Load().(readOnly)    e,ok=read.m[key]    //如果m.read中还是不存在,并且m.dirty中有新数据    if!okread.amended{      //从m.dirty查找      e,ok=m.dirty[key]      //不管m.dirty中存不存在,都将misses计数加一      //missLocked()中满足条件后就会提升m.dirty      m.missLocked()    }    m.mu.Unlock()  }  if!ok{    returnnil,false  }  returne.load()}

这里有两个值的







































北京中科白癜风康复明星
北京中科白癜风康复明星



转载请注明:http://www.92nongye.com/ksfc/204621677.html

  • 上一篇文章:
  •   
  • 下一篇文章: 没有了