前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >无辜的goroutine

无辜的goroutine

作者头像
李海彬
发布2018-03-26 13:19:24
6500
发布2018-03-26 13:19:24
举报
文章被收录于专栏:Golang语言社区Golang语言社区

简介:

本文主要是针对一些对于goroutine的“指控”提出我自己的看法,特别是轩脉刃的一篇博客文章《论go语言中goroutine的使用》提出了goroutine的几宗罪。实际上goroutine确实有增加程序复杂度而容易导致问题之处,特别是死锁;但是另外的一些指控,我认为实际上goroutine是没有直接责任的。

以下就《论go语言中goroutine的使用》的内容一一提出我的观点。

第一个指控:goroutine的指针传递是不安全的

原文:

fun main() {

request := request.NewRequest() //这里的NewRequest()是传递回一个type Request的指针

go saveRequestToRedis1(request)

go saveReuqestToRedis2(request)

select{}

}

func saveRequestToRedis1(request *Request){

request.ToUsers = []int{1,2,3} //这里是一个赋值操作,修改了request指向的数据结构

redis.Save(request)

return

}

这样有什么问题?saveRequestToRedis1和saveReuqestToRedis2两个goroutine修改了同一个共享数据结构,但是由于routine的执行是无序的,因此我们无法保证request.ToUsers设置和redis.Save()是一个原子操作,这样就会出现实际存储redis的数据错误的bug。好吧,你可以说这个saveRequestToRedis的函数实现的有问题,没有考虑到会是使用go routine调用。请再想一想,这个saveRequestToRedis的具体实现是没有任何问题的,它不应该考虑上层是怎么使用它的。那就是我的goroutine的使用有问题,主routine在开一个routine的时候并没有确认这个routine里面的任何一句代码有没有修改了主routine中的数据。对的,主routine确实需要考虑这个情况。但是按照这个思路,所以呢?主goroutine在启用go routine的时候需要阅读子routine中的每行代码来确定是否有修改共享数据??这在实际项目开发过程中是多么降低开发速度的一件事情啊!

我的观点:

1.函数和调用者直接必须遵循一定的规范或者说约定。这个约定包括:

1) 函数签名。这个在强类型的编程语言中可以由编译器保证。

2) 语义。就是调用者要根据函数的用途去调用函数,函数也必须实现自己的目的。如果一个读函数Read实际执行的却是Write操作,就是语义错误。

3) 附带数据的权限控制,包括读写权限和线程(goroutine)安全性。比如参数或者返回的数据由谁来负责控制,函数是不是可以写参数所指向的数据等等。特别在package暴露出来的函数中,对数据的权限说明就特别重要。一个例子是bytes.Buffer.Next方法,在文档中很明确地说明它返回的数据只在下次读写操作前有效。

2.根据1的原则来看上面的例子,就发现saveRequestToRedis1和调用者之间的调用约定并不明确。如果调用约定说明参数指向的数据会被修改,就是调用者的问题;如果调用约定说明参数指向的数据不会被修改,就是函数实现的问题。

3.因此,本例子中的问题实际上是调用约定不明确或者没有遵守的问题,goroutine在这里是无辜的。

第二个指控:goroutine增加了函数的危险系数

原文:

上文说,往一个go函数中传递指针是不安全的。那么换个角度想,你怎么能保证你要调用的函数在函数实现内部不会使用go呢?如果不去看函数体内部具体实现,是没有办法确定的。

例如我们将上面的典型例子稍微改改

func main() {

request := request.NewRequest()

saveRequestToRedis1(request)

saveRequestToRedis2(request)

select{}

}

这下我们没有使用并发,就一定不会出现这问题了吧?追到函数里面去,傻眼了:

func saveReqeustToRedis1(request *Request) {

… go func() {

… request.ToUsers = []{1,2,3} …. redis.Save(request)

}

}

我勒个去啊,里面起了一个goroutine,并修改了request指针指向的对象。这里就产生了错误了。好吧,如果在调用函数的时候,不看函数内部的具体实现,这个问题就无法避免。所以说呢?所以说,从最坏的思考角度出发,每个调用函数理论上来说都是不安全的!试想一下,这个调用函数如果不是自己开发组的人编写的,而是使用网络上的第三方开源代码...确实无法想象找出这个bug要花费多少时间。

我的观点:

1.其实这个问题和第一个指控是类似的,实际问题还是关于数据的权限和goroutine安全性的约定不明确,只是现在是函数调用其他子函数从而本身变成调用者而已。

2.关于goroutine安全性举个例子:database/sql.Stmt的文档说明就有指出“Stmt is safe for concurrent use by multiple goroutines”。

3.那么,使用网上的第三方库怎么办?我的观点是如果它的接口文档简陋没有相关的约定说明,建议这样的库还是不要用了,不然风险太大了。实际上库的质量不仅仅包括代码质量,也包括文档的质量。

第三个指控:goroutine的滥用陷阱

原文:

func main() {

go saveRequestToRedises(request)

}

func saveRequestToRedieses(request *Request) {

for _, redis := range Redises {

go redis.saveRequestToRedis(request)

}

}

func saveRequestToRedis(request *Request) {

…. go func() {

request.ToUsers = []{1,2,3}

… redis.Save(request)

}()

}

神奇啊,go无处不在,好像眨眨眼就在哪里冒出来了。这就是go的滥用,到处都见到go,但是却不是很明确,哪里该用go?为什么用go?goroutine确实会有效率的提升么?

c语言的并发比go语言的并发复杂和繁琐地多,因此我们在使用之前会深思,考虑使用并发获得的好处和坏处。go呢?几乎不。

我的观点:

1.对于package暴露出来的函数,必须在文档(注释)中明确写明函数的调用约定。go标准库就是个比较好的榜样。

2.对于package内部的函数,不需要很明确地写出调用约定。如果是多人开发同一个package,则开发人员有责任去了解被调用函数的默认约定(通过查看函数实现或者简单的约定说明)。

3.在遵守函数约定的前提下,使用goroutine完全不是问题。举个例子:

假设要实现一个排序函数sort,约定是线程不安全的,即不允许把同一个数列在多个goroutine中同时排序。但是我们仍然可以在函数内部使用goroutine:

func sort(numbers []int) {

var wg sync.WaitGroup for i := 0; i < 5; i++ {

wg.Add(1) go func() { // 排序子数组 wg.Done() }()

} wg.Wait() // 合并子数组

}

结论:

1.一些看似由goroutine导致的问题其实不应该归咎于goroutine,那些问题可能是由于不遵守函数调用约定导致的;即使在C/C++里,不遵守函数调用约定一样会导致问题。

2.packge的导出函数特别需要明确函数调用约定,否则会导致调用者误用;而packge内部的函数约定,则需要开发者自己把控(类比于C++中开发者对类的内部函数的责任)。

3.goroutine会导致问题往往是死锁等待等多线程中容易发生的问题。这可以从设计一个良好的设计和良好的代码框架来减少问题的风险,加强代码评审也是一个重要的措施。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-02-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Golang语言社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档