并发编程中的一个陷阱
最近两个多月都在使用golang编写服务器端程序,大量使用到了golang中的协程(goroutine)来处理各种并发事务,非常的方便快捷,的确比原先使用c/c++编写程序效率上提升了很多。goalng提供的闭包也很强大,很容易让人不由自主使用闭包。所谓闭包,就是一种特殊的匿名函数,在闭包中可以直接使用上下文中有效范围内的常量或变量,这些常量或变量一旦被闭包所引用,那么就会延长其生命周期,即使其常规意义上的生命周期已经到来,在闭包中这些值仍然是有效的。闭包使用起来非常方便,但不小心也会掉入陷阱中。就好比一柄利剑,使用得当,就可以所向披靡;使用不当,就会伤及自身。
前两天,在跟前端的同学联调一个刚写完的golang微服务时,发现了一个莫名其妙的问题,对于多个发送人的情况,只有其中一个人能接收到,查看了一下代码,貌似也没有问题啊。
for {
select {
case msg := <- this.smsQueue:
outputStr = fmt.Sprintf("活动[%v]的短信在发送队列中被调度,准备发送...", msg.RequestId)
g_logger.Debug(outputStr)
//发送短信
for _, entry := range msg.Entries {
receiverList := strings.FieldsFunc(entry.Receiver, func (char rune) bool {
switch char {
case ',':
return true
}
return false
})
for _, receiver := range receiverList {
go func() {
g_snsClient.SendSMSMessage(msg.RequestId, strconv.FormatUint(g_smsSerialNumber, 10), receiver, entry.Content, entry.Signature)
}()
}
}
}
}
看似没有问题的代码,经不起推敲啊,稍微思考一下并发的运行机制,就会发现上面的代码存在很大的问题,这里面有一个很典型的并发编程的陷阱:在一个循环体中启动goroutine,而goroutine的执行体又通过闭包引用了循环体的参数,就必然会导致问题。比如上面的代码中内层循环体参数receiver被goroutine的闭包引用,但receiver的值却是随着循环体的执行在变换,这样就导致前面启动的goroutine里的receiver值已经不是期望中的值,都随着外面receiver的改变在改变,这就是为什么多个接收人只有最后一个接收到的原因。同样的问题,外层循环体中的参数entry在闭包中也被直接引用,所以也会产生问题,如果现在还没出现问题,那只是运气好而已,早晚会出现问题的。
这个问题的解决其实也很简单,就是不要依赖于闭包的共享作用域变量的特性,而是通过参数传递来生成Copy的本地变量,让闭包里的值跟外层的循环参数隔离开来,就不会出现这个问题了。
for {
select {
case msg := <- this.smsQueue:
outputStr = fmt.Sprintf("活动[%v]的短信在发送队列中被调度,准备发送...", msg.RequestId)
g_logger.Debug(outputStr)
//发送短信
for _, entry := range msg.Entries {
receiverList := strings.FieldsFunc(entry.Receiver, func (char rune) bool {
switch char {
case ',':
return true
}
return false
})
for _, receiver := range receiverList {
go func(requestid string, smsSerialNumber uint64, phone string, content string, signature string) {
g_snsClient.SendSMSMessage(requestid, strconv.FormatUint(smsSerialNumber, 10), phone, content, signature)
}(msg.RequestId, g_smsSerialNumber, receiver, entry.Content, entry.Signature)
}
}
}
}
这种并发编程的问题并不限于某种语言,只要是支持并发和闭包的高级语言都可能遇到。碰巧的是,我刚刚修改了该问题后不久,团队里一个前端的小盆友就遇到了相同的问题,拉着我一起分析时,我一下就匹配上了这个问题模式,一模一样,她使用的javascript语言。所以在这里记录一下,给自己也给各位同学提个醒。只要意识到了这个问题,还是很容易避免的。