前段时间学习 go, 专门整理的关于 goroutine 的理解.
What's goroutine? It's an independently executing function launched by a go statement.
It has its own call stack, which grows and shrinks as required.
It's very cheap. It's practical to have thousands, even hundreds of thousands of goroutines.
It's not a thread.
There might be only one thread in a program with thousands of goroutines.
Instead, goroutines are multiplexed dynamically onto threads as needed to keep all the goroutines running.
But if you think of it as very cheap thread, you won't be far off.
1. 理解"并发" 不是 "并行"
并发: 同时处理一些事
并行: 同时做一些事
concurrency is the composition of independently executing computations.
concurrency is not parallelism, although it enables parallelism.
If you have only one processor, your program can still be concurrent, but it can't be parallel.
On the other hand, a well-written program might run efficiently in parallel on a multiprocessor. That property could be important...
---Rob Pike
举例: 比如你是一个公司的CEO, 你每天上午会抽出两个小时的时间统一处理回复邮件, 接待下属的各种问题. 那么当你正在回邮件的时候, 有人敲门进来找你签字, 那么你就停下手里的邮件, 把字签了, 然后再继续回来写邮件. 那么在这两个小时里, 你回了好几封邮件, 还签了好几份文件. 而这个过程就可以称为并发. 因为你并没有一边回邮件, 一边签文件, 所以这不是并行.
2. goroutine 的用法
在需要做并发处理的方法名前面加个 go 关键字即可
首先看下没有引入 goroutine 时的情况
package main
import (
"fmt"
"time" ) func say(s string) { for i := 0; i<3; i++ { fmt.Println(s) time.Sleep(time.Millisecond*100) } } func main() { say("Hey") say("There") }
以下为输出, 这里呢, 就是一个顺序执行, 先执行第一个 say 方法, 循环3次输出, 然后执行第二个say方法, 循环3次输出. 但是这不符合我们的预期, 我希望输出的是交替输出. 也就是输出一个 Hey, 输出一个 There ....... 这个需求就有点像我们前面提到的例子, 两个事物需要交替处理
Hey
Hey
Hey
There
There
There
这样就引出 goroutine 来解决这个并发的问题, 但是具体怎么用, 先看几个代码片段来了解一下:
(1)
func main() { say("Hey") go say("There") }
输出, 执行过程: 执行第一个 say() 方法, 连续输出三个 "Hey" ---> 执行第二个 say()方法, 而这里由于 goroutine 会立即返回 ---> 程序接着执行后面的代码, 所以第二个方法还没有执行完毕 ---> 程序退出.
Hey
Hey
Hey
(2)
func main() { go say("Hey") say("There") }
输出, 执行过程: 执行第一个 say() 方法, 根据 goroutine 立即返回的特性 ---> 执行第二个 say()方法, 输出第一个 There ---> 大家别忘了在 say 方法中, 还有这样一行代码
time.Sleep(time.Millisecond*100)
它的作用就是模拟缓慢的函数调用, 让程序暂停指定的时间. 多亏了这个延迟, 让第二个循环输出一次之后, 就给了第一个循环的机会 ---> 输出 Hey, 之后按照上术逻辑继续循环输出.
There
Hey
There
Hey
There
Hey
(3)
func main() { go say("Hey") go say("There") }
输出, 执行过程: 执行第一个 goroutine, 立即返回 ---> 执行第二个 goroutine, 立即返回 ---> 程序退出, 因此没有任何输出.
Process finished with exit code 0
(4)
func main() { go say("Hey") go say("There") // 使程序延迟退出 time.Sleep(time.Second) }
通过 time.Sleep 函数使程序延迟一秒退出, 就给了 goroutine 执行的时间, 这里, 我尝试多次刷新, 输出的顺序都不相同, 同时, 还可以尝试把 say 函数里的 time.Sleep 去掉, 再多次执行, 也会发现输出的顺序都不同.我觉得这也充分体现了goroutine 的并发执行.
输出情况一
Hey
There
There
Hey
There
Hey
输出情况二
There
Hey
Hey
There
Hey
There
总结一下 goroutine 的特点: 通过使用 goroutine, 可以在调用函数之后立即执行后面的代码, 但是使用 goroutine 的函数依然会执行, 但不会阻塞程序中其他代码行的执行.
3. 使用 WaitGroup 函数控制程序的退出
在上面的最后一个例子中, 我们使用了 time.Sleep 人为控制程序延迟1秒退出, 但是在实际开发中, 我们无法去精准的算出, 所有的 goroutine 完成返回需要多少时间, 所以这样认为控制程序的退出时间, 并不是一个好办法, 因此就引出了 sync 包的 WaitGroup 函数. 使用方法详见注释如下:
package main
import (
"fmt"
// 引入sync 包 "sync" "time" ) // 声明 WaitGroup 类型的变量 wg var wg sync.WaitGroup func say(s string) { for i := 0; i<3; i++ { fmt.Println(s) time.Sleep(time.Millisecond*100) } // 告知所有的 goroutine 运行结束, 程序可以返回了 wg.Done() } func main() { //添加一个 goroutine 到 waitgroup wg.Add(1) go say("Hey") //添加一个 goroutine 到 waitgroup wg.Add(1) go say("There") // 告诉程序等待以上添加到 waitGroup 的 goroutine 运行结束再向下执行 wg.Wait() -----以上可以看作是以wg.Wait()为分割的第一个 waitGroup, 以下为第二个---- //添加一个 goroutine 到另一个 waitgroup wg.Add(1) go say("Hi") // 告诉程序等待这个 waitGroup 里的 goroutine 运行结束再向下执行 wg.Wait() }
输出结果如下
There
Hey
There
Hey
There
Hey
Hi
Hi
Hi
4. Defer
defer statement will defer running the function until the end of the surrounding function, either that the function is done, or if the function does happened error panic out, the defer statement will run.
defer 遵循先进后出原则, 所以是倒叙执行
在上面的例子中, 我们看到 wg.Done() 函数是写在 for 循环后面的, 那么如果前面执行中如果出了错误, 程序就永远无法执行到 wg.Done() 函数, 那么整个程序就会无限制地等下去, 对于这个问题, 我们可以把 wg.Done() 函数写在 defer 语句后面, 而且放在函数的最前面,这样不管后面的执行是正常返回, 还是报错, 都会最终执行到 wg.Done() 函数. 这样才能正常向下执行.
func say(s string) { // defer 语句会确保函数 wg.Done() 执行,且是在函数内其他程序之后 defer wg.Done() // 告知所有的 goroutine 运行结束, 程序可以返回 for i := 0; i<3; i++ { fmt.Println(s) time.Sleep(time.Millisecond*100) } }
5. panic 和 recover
func cleanUp() { defer wg.Done() if r := recover(); r != nil { fmt.Println("Recover from cleanUp:", r) } } func say(s string) { defer cleanUp() for i := 0; i<3; i++ { fmt.Println(s) time.Sleep(time.Millisecond*100) if i == 1 { panic("oh my god") } } } func main() { //添加一个 goroutine 到 waitgroup wg.Add(1) go say("Hey") //添加一个 goroutine 到 waitgroup wg.Add(1) go say("There") // 告诉程序等待 goroutine 运行 wg.Wait() fmt.Println("this is something") //添加一个 goroutine 到 waitgroup wg.Add(1) go say("Hi") // 告诉程序等待 goroutine 运行 wg.Wait() }
运行结果, 可以看到由于在 i=1 的时候, 运行 panic()函数, 所以只能循环两次, for 循环遇到 panic 就会停止执行, 然后运行被 defer 的 cleanUp()函数, 在这个函数里, 调用了 recover()函数, 调用 recover 函数, 就会得到 panic()函数的返回值, 我们将此值赋值给了变量 r, 当 r 不等于 nil 的时候, 打印它. 最后再执行被 defer 的 wg.Done() 函数, 告知程序向下执行.
There
Hey
There
Hey
Recover from cleanUp: oh my god
Recover from cleanUp: oh my god
this is something
Hi
Hi
Recover from cleanUp: oh my god
6. Channel
package main
import (
"fmt"
"sync" ) var wg sync.WaitGroup // 用于向通道里传值的函数 func foo(c chan int, val int) { wg.Done() //把接收到的值乘以5后传给指定的通道 c <- val * 5 } func main() { // 创建一个通道, buffer 为10, 也就是可以缓冲 10个值 fooVal := make(chan int, 10) //循环10次调用 foo()函数, 也就是向 fooVal 通道里传了10个值 for i := 1; i < 10; i++ { wg.Add(1) go foo(fooVal, i) } //等待向通道传值的 goroutine 全部完成 wg.Wait() // 通道用完, 需要及时关闭 close(fooVal) // 把通道里的值循环打印出来 for item := range fooVal { fmt.Println(item) } }