当前位置: 首页 > 科技观察

如何使用atomic包减少锁冲突

时间:2023-03-14 20:13:10 科技观察

之前写过这篇文章是基于Golang1.14Go提供了channels或者mutexes等内存同步机制,帮助解决不同的问题。在共享内存的情况下,互斥锁可以保护内存免受数据竞争的影响。然而,尽管存在两个互斥体,Go还通过atomic包提供原子内存原语以提高性能。在深入研究解决方案之前,让我们退后一步,看看数据竞争。当两个或多个goroutine同时访问同一个内存区域,并且其中至少有一个正在写入时,就会发生数据竞争。虽然map内部有一定的机制来防止datarace,但是简单的结构没有任何机制,所以很容易出现datarace。为了说明数据竞争,我以一个goroutine持续更新配置为例来给大家演示一下。packagemainimport("fmt""sync")typeConfigstruct{a[]int}funcmain(){cfg:=&Config{}//启动一个writergoroutine,不断写入数据gofunc(){i:=0for{i++cfg.a=[]int{i,i+1,i+2,i+3,i+4,i+5}}}()//启动多个readergoroutine不断获取数据varwgsync.WaitGroupforn:=0;n<4;n++{wg.Add(1)gofunc(){forn:=0;n<100;n++{fmt.Printf("%#v\n",cfg)}wg.Done()}()}wg.Wait()}运行这段代码,我们可以清楚的看到,运行上面的代码后,每一行的数字应该是连续的,但是由于数据竞争的存在,结果是不确定的。F:\hello>gorunmain.go[...]&main.Config{a:[]int{180954,180962,180967,180972,180977,180983}}&main.Config{a:[]int{181296,181304,181311,181318,181322,181323}}&main.Config{a:[]int{181607,181617,181624,181631,181636,181643}}我们可以在运行时加入参数--race>gorun--racemain.go[...]&main.Config{a:[]int(nil)}==================&main.Config{a:[]int(nil)}WARNING:DATARACE&main.Config{a:[]int(nil)}Readat0x00c00000c210byGoroutine9:reflect.Value.Int()D:/Go/src/reflect/value.go:988+0x3584fmt.(*pp.printValue()D:/Go/src/fmt/print.go:749+0x3590fmt.(*pp).printValue()D:/Go/src/fmt/print.go:860+0x8f2fmt.(*pp.printValue()D:/Go/src/fmt/print.go:810+0x289afmt.(*pp).printValue()D:/Go/src/fmt/print.go:880+0x261cfmt.(*pp).printArg()D:/Go/src/fmt/print.go:716+0x26bfmt.(*pp).doPrintf()D:/Go/src/fmt/print.go:1030+0x326fmt.Fprintf()D:/Go/src/fmt/print.go:204+0x86fmt.Printf()D:/Go/src/fmt/print.go:213+0xbcmain.main.func2()F:/hello/main.go:31+0x42Ppreviouswriteat0x00c00000c210bygoroutine7:main.main.func1()F:/hello/main.go:21+0x66goroutine9(running)createdat:main.main()F:/hello/main.go:29+0x124goroutine7(running)createdat:main.main()F:/hello/main.go:16+0x95===================为了避免同时读写过程中的数据竞争当时,最常用的方法可能是Usemutex或atomicpackageMutex?还是原子的?标准库在sync包中提供了两个互斥量:sync.Mutex和sync.RWMutex。当您的程序需要处理很多读取和很少的写入时,后者会得到优化。对于上面代码中产生的数据竞争问题,我们来看一下,如何解决呢?使用sync.Mutex解决数据竞争//启动一个writergoroutine,不断写入数据gofunc(){i:=0for{i++//写入数据时,先加锁mux.Lock()cfg.a=[]int{i,i+1,i+2,i+3,i+4,i+5}mux.Unlock()}}()//启动多个readergoroutines,不断获取数据varwgsync.WaitGroupforn:=0;n<4;n++{wg.Add(1)gofunc(){forn:=0;n<100;n++{//因为这里只需要读数据,所以只需要给mux.RLock()加读锁fmt.Printf("%#v\n",cfg)mux.RUnlock()}wg.Done()}()}wg.Wait()}通过上面的代码,我们做了两处改动。第一个变化是在写数据前通过mux.Lock()加锁;第二个更改是在读取数据之前通过mux.RLock()添加读取锁定。运行以上代码查看结果:F:\hello>gorun--racemain.go&main.Config{a:[]int{512,513,514,515,516,517}}&main.Config{a:[]int{512,513,514,515,516,517}}&main.Config{a:[]int{513,514,515,516,517,518}}&main.Config{a:[]int{513,514,515,516,517,518}}&main.Config{a:[]int{513,514,515,516,517,518}}&main.Config{a:[]int{513,517,514,65,5515}&main.Config{a:[]int{514,515,516,517,518,519}}[...]这次它达到了我们的预期,没有数据竞争。使用atomic解决数据竞争config{a:[]int{i,i+1,i+2,i+3,i+4,i+5},}v.Store(cfg)}}()//读取数据varwgsync.WaitGroupforn:=0;n<4;n++{wg.Add(1)gofunc(){forn:=0;n<100;n++{cfg:=v.Load()fmt.Printf("%#v\n",cfg)}wg.Done()}()}wg.Wait()}这里我们使用了atomic包,我们发现通过运行它,也达到了我们想要的结果:[...]main.Config{a:[]int{219142,219143,219144,219145,219146,219147}}main.Config{a:[]int{219491,219492,219493,219494,219495,219496}}main.Config{a:[]int{219826,219827,219828,219829,219830,219831}}main.Config{a:[]int{219948,219949,219950,219951,219952,219953}}从生成的输出来看,似乎使用了原子包的解决方案要快得多,因为它可以生成更大数字的序列。为了更严格地证明这个结果,下面我们将对这两个程序进行benchmark。性能分析基准应该根据测量的内容来解释。因此,我们假设在前面的程序中,有一个datawriter不断的存储新的配置,还有多个datareader不断的读取配置。为了涵盖更多潜在场景,我们还将包括一个仅包含数据读取器的基准测试,假设配置不经常更改。以下是基准代码的一部分:funcBenchmarkMutexMultipleReaders(b*testing.B){varlastValueuint64varmuxsync.RWMutexvarwgsync.WaitGroupcfg:=Config{a:[]int{0,0,0,0,0,0},}forn:=0;n<4;n++{wg.Add(1)gofunc(){forn:=0;n