您的位置 首页 golang

Go语言协程使用最佳实践

  • Go语言协程使用
    • 前言
    • 协程的使用及控制
    • 协程崩溃处理
    • 协程超时控制
    • 是否可以无限多开协程
    • 高并发下情况下如何开协程
    • 结尾

Go语言协程使用

前言

Go语言的真正精髓,莫过于go协程和channel.因此对于goroutine和channel的正确使用,非常重要.是编写高并发程序的重要基础.本文试图将协程的使用方方面面讲清楚.

全文阅读大概需要30分钟.如时间不够,欢迎收藏后阅读.

协程的使用及控制

在Go语言中开一个协程非常方便,在需要通过协程来执行的函数时,直接在函数前加go关键字就可以

 package main
import (
"fmt"
)

func A(i int) {
fmt.Println("我是A")
}
func main() {
fmt.Println("我是main")
go A(1)
    fmt.Println("执行完了")
}
  

执行后输出

 我是main
执行完了
  

程序正常执行没有报错,但是没有函数A的输出.

这是因为主协程并不会等待子协程执行完才往下走,执行到go后,语句会继续执行,go后面的函数新开一个协程,各跑各的,所以主协程执行完go语句,就无事可做,就退出了.

那怎么让上面的代码打印出函数A的输出.得让主函数待一会儿协程执行完了再退出,或者让主协程不退出,比如在web程序中,主协程是不退出的.

  • 通过 sync. WaitGroup 的三个方法 Add() , Done() , Wait() 来实现协程的控制
  • 通过带buffer的channel来控制
  • 通过sync. Cond

下面演示下等待协程完成的代码

 package main

import (
"fmt"
"sync"
)

func A(i int) {
fmt.Println("我是A", i)
}
func main() {
var wg sync.WaitGroup
fmt.Println("我是main")
wg.Add(1)
go func(i int) {
defer wg.Done()
A(i)
}(1)
wg.Wait()
fmt.Println("执行完了")
}
  
 我是main
我是A 1
执行完了
  

下面演示通过channel来控制协程的流程

 package main

import (
"fmt"
)

func A(i int) {
fmt.Println("我是A", i)
}

func main() {
ch := make(chan bool, 1)
fmt.Println("我是main")
go func(i int, chp chan<- bool) {
defer close(chp)
A(i)
fmt.Println("finish")
chp <- true

}(1, ch)
fmt.Println("wait")
<-ch
fmt.Println("执行完了")
}

  
 我是main
wait
我是A 1
finish
执行完
  

下面演示通过sync. Cond来实现

 package main

import (
"fmt"
"sync"
)

func A(i int) {
fmt.Println("我是A", i)
}

func main() {
var locker = new(sync.Mutex)
var cond = sync.NewCond(locker)
var done bool = false
fmt.Println("我是main")
cond.L.Lock()

go func(i int) {
A(i)
fmt.Println("finish")
done = true
cond.Signal()

}(1)
fmt.Println("wait")
if !done {
cond.Wait()
cond.L.Unlock()
}
fmt.Println("执行完了")
}

  
 我是main
wait
我是A 1
finish
执行完了
  

代码示例:

协程崩溃处理

在Go语言中,如果一个协程崩溃了,则所有协程都会退出,比如数组越界,会触发panic(相当于throw exception), 这对持续可运行的应用来说,显然不是我们想要的效果.那这个时候我们需要对崩溃进行修复.在Go语言中提供了一个defer和recover来实现崩溃恢复,这个相当于其它语言的try catch的方式.
在使用recover函数时,如果要达到能捕获异常的作用,有几点需要注意:

  • recover如果想起作用的话, 必须在defered函数前声明,因为只要panic,后面的函数不会被执行
  • recover函数只有在方法内部发生panic时,返回值才不会为nil,没有panic的情况下返回值为nil

下面用代码示例来说明情况

 package main

import (
"fmt"
"sync"
)

func A(i int) {
fmt.Println("我是A", i)
panic("崩溃")
defer func() { //在panic后声明defer,不能捕获异常
if err := recover(); err != nil {
fmt.Println("恢复", err)
}
}()

}
func main() {
var wg sync.WaitGroup
fmt.Println("我是main")
wg.Add(1)
go func(i int) {
defer wg.Done()
A(i)

}(1)
wg.Wait()

fmt.Println("执行完了")
}

  

输出:

 ./prog.go:11:2: unreachable code
Go vet exited.

我是main
我是A 1
panic: 崩溃

goroutine 6 [running]:
main.A(0x1)
/tmp/sandbox516871981/prog.go:10 +0xc5
main.main.func1(0xc000018040, 0x1)
/tmp/sandbox516871981/prog.go:24 +0x53
created by main.main
/tmp/sandbox516871981/prog.go:22 +0xd5

  

再看在panic前声明recover

 package main

import (
"fmt"
"sync"
)

func A(i int) {
defer func() { //在panic前声明defer,能捕获异常
if err := recover(); err != nil {
fmt.Println("恢复", err)
}
}()
fmt.Println("我是A", i)
panic("崩溃")

}
func main() {
var wg sync.WaitGroup
fmt.Println("我是main")
wg.Add(1)
go func(i int) {
defer wg.Done()
A(i)

}(1)
wg.Wait()

fmt.Println("执行完了")
}

  

输出

 我是main
我是A 1
恢复 崩溃
执行完了

  

此时的panic被捕获了

defer recover函数必须放在需要捕获panic的函数前面

因此本示例如果将defer recover放在go func函数中被调用函数f前面,也能捕获住A函数的panic

 package main

import (
"fmt"
"sync"
)

func A(i int) {
fmt.Println("我是A", i)
panic("崩溃")

}
func main() {
var wg sync.WaitGroup
fmt.Println("我是main")
wg.Add(1)
go func(i int) {
defer func() { //在调用A函数前声明defer recover,能捕获异常
if err := recover(); err != nil {
fmt.Println("恢复", err)
}
wg.Done()
}()
A(i)

}(1)
wg.Wait()

fmt.Println("执行完了")
}

  

输出

 我是main
我是A 1
恢复 崩溃
执行完了
  

因此,如果在协程内执行其它函数时,为了保证不崩溃,安全的做法是,提前声明defer recover函数
这样可以保证协程内部崩溃,不会将整个进程崩溃掉

协程超时控制

当你希望控制一个协程的执行时间,如果超过指定时间,还没有执行完,则退出.直接返回超时错误,这个该如何做呢?

通行做法是用select + channel来进行超时控制,
channel发执行完毕的信号,然后超时信号通用ctx. Done()或者time. After(), 或者time. Ticket()来完成超时通知退出,select捕获到其中一个channel有数据,就执行对应的代码,然后退出.

其中一个注意的点是, channel要用有缓冲的,不然,在超时分支退出时,协程还在卡住,造成goroutine泄露.

代码示例如下

 package main

import (
"context"
"fmt"
"sync"
"time"
)

func Do(ctx context.Context, wg *sync.WaitGroup) {
ctx, cancle := context.WithTimeout(ctx, time.Second*2)
defer func() {
cancle()
wg.Done()
}()

done := make(chan struct{}, 1) //执行成功的channel
go func(ctx context.Context) {
fmt.Println("go goroutine")
time.Sleep(time.Second * 10)
done <- struct{}{} //发送完成的信号
}(ctx)

select {
case <-ctx.Done(): //超时
fmt.Printf("timeout,err:%v\n", ctx.Err())
case <-time.After(3 * time.Second): //超时第二种方法
fmt.Printf("after 1 sec.")
case <-done: //程序正常结束
fmt.Println("done")
}

}

func main() {
fmt.Println("main")
ctx := context.Background()
var wg sync.WaitGroup
wg.Add(1)
Do(ctx, &wg)
wg.Wait()
fmt.Println("finish")
}

  

输出如下:

 main
go goroutine
timeout,err:context deadline exceeded
finish
  

程序执行了超时退出

是否可以无限多开协程

众所周知,协程不同于线程,并不和操作系统的线程有具体的对应关系.协程是由go的一个线程池来调度的.

go runtime并不会产生一个协程对应产生一个os线程,是一个m:n的对应关系,根据m:n对应关系,协程对应的os线程runtime. GOMAXPROCS默认为系统逻辑cpu数量,因此创建更多的m并不会产生更多的操作系统线程,但是可以通过runtime. GOMAXPROCS()来设置当前程序运行时占用的系统核心数

协程创建需要占用一定量的内存,开一个协程只需要少量的内存空间,即KB,这也是golang能实现百万长链的原因.

但在实际中,协程需要正确的关闭,而不是无限创建后,造成协程泄露,进而引发系统崩溃.

高并发下情况下如何开协程

在高并发情况下,需要通过一个带缓冲的channel的来实现对于协程的创建数量进行控制,进而实现一个健康稳定的可持续运行的高并发处理程序.

结尾

现代语言的发展,从进程到线程,进而到协程.各种语言的诞生,一直致力于2个方面的努力.

  • 更高效率地利用CPU
  • 更低成本地实现并发

目前来看,Go语言的协程则是这2个思路的最佳实践.go的协程和channel是Go语言的真正精髓.掌握好go+channel的使用,有利于更加准确的开发高效的并发处理程序.

全文完,感谢您的阅读.

如有不正确的地方,欢迎留言讨论.

文章来源:智云一二三科技

文章标题:Go语言协程使用最佳实践

文章地址:https://www.zhihuclub.com/86279.shtml

关于作者: 智云科技

热门文章

网站地图