《从 bug 中学习:六大开源项目告诉你 go 并发编程的那些坑.docx》由会员分享,可在线阅读,更多相关《从 bug 中学习:六大开源项目告诉你 go 并发编程的那些坑.docx(8页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、从bug中学习:六大开源项目告诉你go并发编程的那些坑richardyao腾讯CSIG后台开发工程师并发编程中go不仅仅支持传统的通过分享内存的方式来通信更推崇通过channel来传递消息这种新的并发编程模型会出现不同于以往的bug。从bug中学习?UnderstandingReal-WorldConcurrencyBugsinGo?这篇paper在分析了六大开源工程并发相关的bug之后为我们总结了go并发编程中常见的坑。别往坑里跳编程更美妙。在go中创立goroutine非常简单在函数调用前加go关键字这个函数的调用就在一个单独的goroutine中执行了go支持匿名函数让创立gorouti
2、ne的操作更加简洁。另外在并发编程模型上go不仅仅支持传统的通过分享内存的方式来通信更推崇通过channel来传递消息Donotcommunicatebysharingmemory;instead,sharememorybycommunicating.这种新的并发编程模型会带来新类型的bug从bug中学习?UnderstandingReal-WorldConcurrencyBugsinGo?这篇paper在Docker、Kubernetes、etcd、gRPC、CockroachDB、BoltDB六大开源工程的commitlog中搜索等关键字race、deadlock、synchronizat
3、ion、concurrency、lock、mutex、atomic、compete、context、once、goroutineleak找出这六大工程中并发相关的bug然后归类这些bug总结出了go并发编程中常见的一些坑。通过学习这些坑可以让我们在以后的工程里防范类似的错误或遇到类似问题的时候可以帮助指导快速定位排查。unbufferedchannel由于receiver退出导致sender侧block如下面一个bug的例子funcfinishReq(timeouttime.Duration)obch:make(chanob)gofunc()result:fn()ch-result/block
4、()selectcaseresult-ch:returnresultcase-time.After(timeout):returnnil本意是想调用fn()时加上超时的功能假如fn()在超时时间没有返回那么返回nil。但是当超时发生的时候针对代码中第二行创立的ch来讲由于已经没有receiver了第5行将会被block住导致这个goroutine永远不会退出。Ifthecapacityiszeroorabsent,thechannelisunbufferedandcommunicationsucceedsonlywhenbothasenderandreceiverareready.Otherw
5、ise,thechannelisbufferedandcommunicationsucceedswithoutblockingifthebufferisnotfull(sends)ornotempty(receives).这个bug的修复方式也是非常的简单把unbufferedchannel修改成bufferedchannel。funcfinishReq(timeouttime.Duration)obch:make(chanob,1)gofunc()result:fn()ch-result/block()selectcaseresult-ch:returnresultcase-time.Aft
6、er(timeout):returnnil考虑在上面的例子中固然这样不会block了但是channel一直没有被关闭channel保持不关闭是否会导致资源的泄漏呢WaitGroup误用导致阻塞下面是一个WaitGroup误用导致阻塞的一个bug的例子s:/github/moby/moby/pull/25384vargroupsync.WaitGroupgroup.Add(len(pm.plugins)for_,p:rangepm.pluginsgofunc(p*plugin)defergroup.Done()(p)group.Wait()当len(pm.plugins)大于等于2时第7行将会被
7、卡住因为这个时候只启动了一个异步的goroutinegroup.Done()只会被调用一次group.Wait()将会永久阻塞。修复如下vargroupsync.WaitGroupgroup.Add(len(pm.plugins)for_,p:rangepm.pluginsgofunc(p*plugin)defergroup.Done()(p)group.Wait()context误用导致资源泄漏如下面的代码所示hctx,hcancel:context.WithCancel(ctx)iftimeout0hctx,hcancelcontext.WithTimeout(ctx,timeout)第一
8、行context.WithCancel(ctx)有可能会创立一个goroutine来等待ctx是否Done假如parent的ctx.Done()的话cancel掉child的context。也就是讲hcancel绑定了一定的资源不能直接覆盖。Cancelingthiscontextreleasesresourcesassociatedwithit,socodeshouldcallcancelassoonastheoperationsrunninginthisContextcomplete.这个bug的修复方式是varhctxcontext.Contextvarhcancelcontext.Ca
9、ncelFunciftimeout0hctx,hcancelcontext.WithTimeout(ctx,timeout)elsehctx,hcancelcontext.WithCancel(ctx)或hctx,hcancel:context.WithCancel(ctx)iftimeout0hcancel.Cancel()hctx,hcancelcontext.WithTimeout(ctx,timeout)多个goroutine同时读写分享变量导致的bug如下面的例子fori:17;i21;i/writegofunc()/*Createanewgoroutine*/apiVersion:
10、fmt.Sprintf(v1.%d,i)/read()第二行中的匿名函数形成了一个闭包(closures)在闭包内部可以访问定义在外面的变量如上面的例子中第1行在写i这个变量在第3行在读i这个变量。这里的关键的问题是对同一个变量的读写是在两个goroutine里面同时进展的因此是不平安的。Functionliteralsareclosures:theymayrefertovariablesdefinedinasurroundingfunction.Thosevariablesarethensharedbetweenthesurroundingfunctionandthefunctionlite
11、ral,andtheysurviveaslongastheyareaccessible.可以修改成fori:17;i21;i/writegofunc(iint)/*Createanewgoroutine*/apiVersion:fmt.Sprintf(v1.%d,i)/read(i)通过passedbyvalue的方式躲避了并发读写的问题。channel被关闭屡次引发的bugs:/github/moby/moby/pull/24007/filesselectcase-c.closed:default:close(c.closed)上面这块代码可能会被多个goroutine同时执行这段代码的逻辑
12、是case这个分支判断closed这个channel是否被关闭了假如被关闭的话就什么都不做假如closed没有被关闭的话就执行default分支关闭这个channel多个goroutine并发执行的时候有可能会导致closed这个channel被关闭屡次。Forachannelc,thebuilt-infunctionclose(c)recordsthatnomorevalueswillbesentonthechannel.Itisanerrorifcisareceive-onlychannel.Sendingtoorclosingaclosedchannelcausesarun-timepa
13、nic.这个bug的修复方式是Once.Do(func()close(c.closed)把整个select语句块换成Once.Do保证channel只关闭一次。timer误用产生的bug如下面的例子timer:time.NewTimer(0)ifdur0timertime.NewTimer(dur)selectcase-timer.C:case-ctx.Done():returnnil原意是想dur大于0的时候设置timer超时时间但是timer:time.NewTimer(0)导致timer.C立即触发。修复后vartimeout-chantime.Timeifdur0timeouttime
14、.NewTimer(dur).Cselectcase-timeout:case-ctx.Done():returnnilAnilchannelisneverreadyforcommunication.上面的代码中第一个case分支timeout有可能是个nil的channelselect在nil的channel上这个分支不会被触发因此不会有问题。读写锁误用引发的buggo语言中的RWMutexwritelock有更高的优先级IfagoroutineholdsaRWMutexforreadingandanothergoroutinemightcallLock,nogoroutineshoulde
15、xpecttobeabletoacquireareadlockuntiltheinitialreadlockisreleased.Inparticular,thisprohibitsrecursivereadlocking.Thisistoensurethatthelockeventuallybecomesavailable;ablockedLockcallexcludesnewreadersfromacquiringthelock.假如一个goroutine拿到了一个readlock然后另外一个goroutine调用了Lock第一个goroutine再调用readlock的时候会死锁应予以防止。参考资料s:/songlh.github.io/paper/go-study.pdfs:/golang.org/ref/specs:/golang.org/doc/effective_go.htmls:/golang.org/pkg/欢送关注腾讯程序员视频号腾讯技术工程