Go在语言层面通过Goroutine与channel来支持并发编程,使并发编程看似变得异常简单,但通过最近一段时间的编码,越来越觉得简单的东西,很容易会被滥用。Java的标准库也让多线程编程变得简单,但想当初在公司定位Java的问题,发现很多的同学由于没有深入了解Java Thread的机制,Thread直接New从不管理复用,那Goroutine肯定也要面临这类的问题。

1 Goroutine泄漏问题

Rob Pike在2012年的Google I/O大会上所做的“Go Concurrency Patterns”的演讲上,说道过几种基础的并发模式。从一组目标中获取第一个结果就是其中之一。

func First(query string, replicas ...Search) Result {
c := make(chan Result)
searchReplica := func(i int) { c <- replicas[i](query) }
for i := range replicas {
go searchReplica(i)
}
return <-c
}

在First()函数中的结果channel是没缓存的。这意味着只有第一个goroutine返回。其他的goroutine会困在尝试发送结果的过程中,如果你有不止一个的重复时,每个调用将会泄露资源。为了避免泄露,你需要确保所有的goroutine退出。一个不错的方法是使用一个有足够保存所有缓存结果的channel。

func First(query string, replicas ...Search) Result {
c := make(chan Result,len(replicas))
searchReplica := func(i int) { c <- replicas[i](query) }
for i := range replicas {
go searchReplica(i)
}
return <-c
}

另一个不错的解决方法是使用一个有default情况的select语句和一个保存一个缓存结果的channel。default情况保证了即使当结果channel无法收到消息的情况下,goroutine也不会堵塞。

func First(query string, replicas ...Search) Result {
c := make(chan Result,1)
searchReplica := func(i int) {
select {
case c <- replicas[i](query):
default:
}
}
for i := range replicas {
go searchReplica(i)
}
return <-c
}

你也可以使用特殊的取消channel来终止workers。

func First(query string, replicas ...Search) Result {
c := make(chan Result)
done := make(chan struct{})
defer close(done)
searchReplica := func(i int) {
select {
case c <- replicas[i](query):
case <- done:
}
}
for i := range replicas {
go searchReplica(i)
}
return <-c
}

为何在演讲中会包含这些bug?Rob Pike仅仅是不想把演示复杂化。这么做是合理的,但对于Go新手而言,可能会直接使用类似代码,而不去思考它可能有问题。

2 Goroutine Race问题

Go语言支持函数中定义函数,看下一个例子:

func saveRequest(request *Request) {
….
go func() {
request.Users = []{1,2,3}

db.Save(request)
} }

很多情况下,由于程序员对goroutine了解不够深入,又由于goroutine使用很容易。为了性能,很容易把一个同步函数变成异步函数,但这违背了go”不要通过共享内存来通信,相反应该通过通信来共享内存“的原则。即上述的例子中起了一个goroutine,并修改了request指针指向的对象。即使对request只读,也可能不是安全,因为你无法保证request指针不在其它goroutine中修改。

在本质上讲,goroutine的使用会增加了函数的危险系数,尤其是函数参数传递指针时。任何一个对象的操作,如果没有加上锁,当项目比较庞大时,可能不知道这个对象是不是会引起多个goroutine竞争。

什么是goroutine race(竞争)问题?官网的文章 Introducing the Go Race Detect给出的例子如下:

package main

import(
"time"
"fmt"
"math/rand"
) func main() {
start := time.Now()
var t *time.Timer
t = time.AfterFunc(randomDuration(), func() {
fmt.Println(time.Now().Sub(start))
t.Reset(randomDuration())
})
time.Sleep(5 * time.Second)
} func randomDuration() time.Duration {
return time.Duration(rand.Int63n(1e9))
}

这个例子看起来没任何问题,但是实际上,time.AfterFunc是会另外启动一个goroutine来进行计时和执行func()。由于func中有对t(Timer)进行操作(t.Reset),而主goroutine也有对t进行操作(t=time.After)。 这个时候,其实有可能会造成两个goroutine对同一个变量进行竞争的情况。

那什么才是goroutine的使用正确姿势,怎么理解“通过通信来共享内存”来避免Race问题?先看一个例子:

type SimpleAccount struct{
balance int
} func NewSimpleAccount(balance int) *SimpleAccount {
return &SimpleAccount{balance: balance}
} func (acc *SimpleAccount) Deposit(amount uint) {
acc.setBalance(acc.balance + int(amount))
} func (acc *SimpleAccount) Withdraw(amount uint) {
if acc.balance >= int(amount) {
acc.setBalance(acc.balance - int(amount))
} else {
panic("杰克穷死")
}
} func (acc *SimpleAccount) Balance() int {
return acc.balance
} func (acc *SimpleAccount) setBalance(balance int) {
acc.balance = balance
} type ConcurrentAccount struct {
account *SimpleAccount
deposits chan uint
withdrawals chan uint
balances chan chan int
} func NewConcurrentAccount(amount int) *ConcurrentAccount{
acc := &ConcurrentAccount{
account : &SimpleAccount{balance: amount},
deposits: make(chan uint),
withdrawals: make(chan uint),
balances: make(chan chan int),
}
acc.listen() return acc
} func (acc *ConcurrentAccount) Balance() int {
ch := make(chan int)
acc.balances <- ch
return <-ch
} func (acc *ConcurrentAccount) Deposit(amount uint) {
acc.deposits <- amount
} func (acc *ConcurrentAccount) Withdraw(amount uint) {
acc.withdrawals <- amount
} func (acc *ConcurrentAccount) listen() {
go func() {
for {
select {
case amnt := <-acc.deposits:
acc.account.Deposit(amnt)
case amnt := <-acc.withdrawals:
acc.account.Withdraw(amnt)
case ch := <-acc.balances:
ch <- acc.account.Balance()
}
}
}()
}

上面的例子,SimpleAccount所有方法,当多goroutine操作是不安全的,而通过ConcurrentAccount封装,所有处理都统一通过channel通信到listen开启的goroutine,即只有一个goroutine能操作SimpleAccount中成员变量,那也就不会发现Goroutine Race问题。

Goroutine陷阱的更多相关文章

  1. GO 新开发者要注意的陷阱和常见错误

    转自:http://colobu.com/2015/09/07/gotchas-and-common-mistakes-in-go-golang/ 初级 开大括号不能放在单独的一行 未使用的变量 未使 ...

  2. Go的50度灰:Golang新开发者要注意的陷阱和常见错误(转)

    目录 [−] 初级 开大括号不能放在单独的一行 未使用的变量 未使用的Imports 简式的变量声明仅可以在函数内部使用 使用简式声明重复声明变量 偶然的变量隐藏Accidental Variable ...

  3. Goroutine(协程)为何能处理大并发?

    简单来说:协程十分轻量,可以在一个进程中执行有数以十万计的协程,依旧保持高性能. 进程.线程.协程的关系和区别: 进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度. 线程拥有自己独 ...

  4. [转载][翻译]Go的50坑:新Golang开发者要注意的陷阱、技巧和常见错误[2]

    Golang作为一个略古怪而新的语言,有自己一套特色和哲学.从其他语言转来的开发者在刚接触到的时候往往大吃苦头,我也不例外.这篇文章很细致地介绍了Golang的一些常见坑点,读完全篇中枪好多次.故将其 ...

  5. [转载]Go的50度灰:Golang新开发者要注意的陷阱和常见错误

    初级 开大括号不能放在单独的一行 未使用的变量 未使用的Imports 简式的变量声明仅可以在函数内部使用 使用简式声明重复声明变量 偶然的变量隐藏Accidental Variable Shadow ...

  6. Go的50坑:新Golang开发者要注意的陷阱、技巧和常见错误[2]

    初级篇 开大括号不能放在单独的一行 未使用的变量 未使用的Imports 简式的变量声明仅可以在函数内部使用 使用简式声明重复声明变量 偶然的变量隐藏Accidental Variable Shado ...

  7. [转]Go的50坑:新Golang开发者要注意的陷阱、技巧和常见错误-高级

    from : https://levy.at/blog/11 进阶篇 关闭HTTP的响应 level: intermediate 当你使用标准http库发起请求时,你得到一个http的响应变量.如果你 ...

  8. Golang新开发者要注意的陷阱和常见错误

    转自:http://colobu.com/2015/09/07/gotchas-and-common-mistakes-in-go-golang/ 目录 [−] 初级 开大括号不能放在单独的一行 未使 ...

  9. TODO:Go语言goroutine和channel使用

    TODO:Go语言goroutine和channel使用 goroutine是Go语言中的轻量级线程实现,由Go语言运行时(runtime)管理.使用的时候在函数前面加"go"这个 ...

随机推荐

  1. 解决IE7兼容H5新标签的方法

    外部引入JS <script src="http://cdn.bootcss.com/html5shiv/r29/html5.min.js"></script&g ...

  2. 基于condition 实现的线程安全的优先队列(python实现)

    可以把Condiftion理解为一把高级的琐,它提供了比Lock, RLock更高级的功能,允许我们能够控制复杂的线程同步问题.threadiong.Condition在内部维护一个琐对象(默认是RL ...

  3. 收藏 - android

    收藏 - android开发 2018-05-04 16:39:36 介绍:这篇文章是收藏系列的开山第一篇,主要收藏了跟android开发有关的一些内容,也算是内容汇总,后期会持续更新: 内容目录 1 ...

  4. add two numbers(将两个链表相加)

    You are given two non-empty linked lists representing two non-negative integers. The digits are stor ...

  5. Mac 电脑前端环境配置

    恍惚间,好久没有在外面写过随笔了.在阿里的那两年,学到了许多,也成长了许多,认识了很多可爱的人,也明白了很多社会的事.最后种种艰难抉择,我来到了美团成都,一个贫穷落后但更自由开放弹性的地方.已经误以为 ...

  6. windows10不能修改hosts解决方案(亲测)

    hosts文本解释: 有时候我们要破解一些软件与服务器通讯,所以通常都需要更改Hosts文件来达到目的,XP系统可以直接修改保存,但是Win10系统却提示没有权限去修改,那么我们要怎样办呢,我们修改的 ...

  7. [开源]基于ffmpeg和libvlc的视频剪辑、播放器

    [开源]基于ffmpeg和libvlc的视频剪辑.播放器 以前研究的时候,写过一个简单的基于VLC的视频播放器.后来因为各种项目,有时为了方便测试,等各种原因,陆续加了一些功能,现在集成了视频播放.视 ...

  8. node七-required、缓存

    学会查API,远比会几个API更重要. 核心模块意义 -如果只是在服务器运行javascript代码,并没有多大意义,因为无法实现任何功能>读写文件.访问网络 -Node的用处在于它本身还提供可 ...

  9. Node笔记二

    ### 安装包的方式安装 - 安装包下载链接: + Mac OSX: [darwin](http://npm.taobao.org/mirrors/node/v5.7.0/node-v5.7.0.pk ...

  10. cocos2d-x工作小记

    1.当一个layer跳到下一个layer时,需要传递数据,可以默认定义一个setUserData()方法. 2.cocos2d-x不使用传统的值类型,所有的对象都创建在堆上,然后通过指针引用. 3.传 ...