[Go语言]无辜的goroutine

简介:
本文主要是针对一些对于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会导致问题往往是死锁等待等多线程中容易发生的问题。这可以从设计一个良好的设计和良好的代码框架来减少问题的风险,加强代码评审也是一个重要的措施。

本文来自:新浪博客

感谢作者:stevewang

查看原文:[Go语言]无辜的goroutine

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。