Go语言实现安全计数的几种方法本文转载请联系Golang公众号。原文如下:有一天,我在研究一个使用C++互斥锁的共享计数器的简单经典实现,对其他线程安全的实现非常好奇。我通常使用Go来满足我的好奇心。本文总结了如何以goroutine安全的方式实现计数器。不要这样做让我们从一个不安全的实现开始:typeNotSafeCounterstruct{numberuint64}funcNewNotSafeCounter()Counter{return&NotSafeCounter{0}}func(c*NotSafeCounter)Add(numuint64){c.number=c.number+num}func(c*NotSafeCounter)Read()uint64{returnc.number}代码没有什么特别之处。让我们测试结果是否正确:创建100个goroutine,其中三分之二递增共享计数器。functestCorrectness(t*testing.T,counterCounter){wg:=&sync.WaitGroup{}fori:=0;i<100;i++{wg.Add(1)ifi%3==0{gofunc(counterCounter){counter.Read()wg.Done()}(counter)}elseifi%3==1{gofunc(counterCounter){counter.Add(1)counter.Read()wg.Done()}(counter)}else{gofunc(counterCounter){counter.Add(1)wg.Done()}(counter)}}wg.Wait()ifcounter.Read()!=66{t.Errorf("countershouldbe%dandwas%d",66,counter.Read())}}测试的结果是不确定的,有时它工作正常,有时会出现如下错误:counter_test.go:34:countershouldbe66andwas65ClassicImplementation实现正确计数器的传统方法是使用互斥锁来确保任何时候都只有一个协程操作计数器。在Go中,我们可以使用sync包。typeMutexCounterstruct{mu*sync.RWMutexnumberuint64}funcNewMutexCounter()Counter{return&MutexCounter{&sync.RWMutex{},0}}func(c*MutexCounter)Add(numuint64){c.mu.Lock()deferc.mu.Unlock()c.nu??mber=c.number+num}func(c*MutexCounter)Read()uint64{c.mu.RLock()deferc.mu.RUnlock()returnc.number}现在测试结果每次都可以通过并且是正确的.使用通道锁是保证同步的低级原语。Go还提供了一个更高级的实现——通道。关于互斥锁和通道,有太多这样的讨论:“互斥锁vs通道”,“哪个更好”,“我应该使用哪个”等等。其中一些讨论非常有趣且内容丰富,但那不是本文的重点。我们使用通道来实现协程安全的计数器,并将通道用作队列。对计数器的操作(读和写)都缓存在队列中,按顺序操作。具体操作是通过传递func()来实现的。创建后,计数器会生成一个goroutine并按顺序执行队列中的操作。下面是计数器的定义:typeChannelCounterstruct{chchanfunc()numberuint64}funcNewChannelCounter()Counter{counter:=&ChannelCounter{make(chanfunc(),100),0}gofunc(counter*ChannelCounter){forf:=rangecounter.ch{f()}}(counter)returncounter}当协程调用Add()时,它向队列添加一个写操作:func(c*ChannelCounter)Add(numuint64){c.ch<-func(){c.number=c.number+num}}当一个协程调用Read()时,一个读操作被添加到队列中:func(c*ChannelCounter)Read()uint64{ret:=make(chanuint64)c.ch<-func(){ret<-c.numberclose(ret)}return<-ret}我真正喜欢这个实现的地方在于它非常清楚顺序执行。原子方式我们甚至可以使用更低级别的原语来使用sync/atomic包执行原子操作。typeAtomicCounterstruct{numberuint64}funcNewAtomicCounter()Counter{return&AtomicCounter{0}}func(c*AtomicCounter)Add(numuint64){atomic.AddUint64(&c.number,num)}func(c*AtomicCounter)Read()uint64{returnatomic.LoadUint64(&c.number)}CompareandExchange或者,我们可以使用一个非常经典的原语:CAS,来对一个计时器进行计数。func(c*CASCounter)Add(numuint64){for{v:=atomic.LoadUint64(&c.number)ifatomic.CompareAndSwapUint64(&c.number,v,v+num){return}}}func(c*CASCounter)Read()uint64{returnatomic.LoadUint64(&c.number)}如何实现float类型在我探索和学习的过程中,看到了一个非常好的视频——。在视频的最后,讨论了如何实现浮点计数器。到目前为止,所有技术都适用于浮点数,除了sync/atomic包,它还没有提供对浮点数的原子操作。在视频中,Bj?rnRabenstein展示了如何通过将浮点数存储为uint64并使用math.Float64bits和math.Float64frombits在float64和uint64之间进行转换来解决此问题。typeCASFloatCounterstruct{numberuint64}funcNewCASFloatCounter()*CASFloatCounter{return&CASFloatCounter{0}}func(c*CASFloatCounter)Add(numfloat64){for{v:=atomic.LoadUint64(&c.number)newValue:=math.Fbits.Float64bits(4v)+num)ifatomic.CompareAndSwapUint64(&c.number,v,newValue){return}}}func(c*CASFloatCounter)Read()float64{returnmath.Float64frombits(atomic.LoadUint64(&c.number))}最后这篇文章是共享计数器的实施摘要。这是我的好奇心的结果,除了对并发有一个基本的了解。如果您有其他实现共享计数的方法,请告诉我。本文提到的实现对应的代码可以在这里看到[2],还包括运行用例和基准测试。参考文献[1]Prometheus:DesigningandImplementingaModernMonitoringSolutioninGo:https://www.youtube.com/watch?v=1V7eJ0jN8-E[2]看这里:https://github.com/brunocalza/sharedcountervia:https://brunocalza.me/there-are-many-ways-to-safely-count/作者:BRUNOCALZA四哥水平有限,如有翻译或理解错误,请帮忙指出,谢谢你!
