[关闭]
@leowenyang 2015-10-24T11:54:40.000000Z 字数 6655 阅读 8427

鸣人学影分身之术 -- 协程 (初级班)

go


  1. 转载说明: 本文章可以任意转载,但转载请说明出处和作者 @leowenyang
  2. 如有什么疑问,欢迎来信 leowenyang@163.com

第一天 初识‘分身术’ - 协程

依鲁卡:鸣人,今天老师教你一招新的忍术
鸣人: 什么忍术呀,酷不酷,厉害不厉害
依鲁卡: 分身术
鸣人: 分身术,把身体分离吗?,那我不就挂了。不学不学。。。
依鲁卡: 。。。
依鲁卡: 分身术是忍术中比较牛逼的一种忍术,学会者可以产生一个和本体一模一样的人,这样就可以两个人进行攻击
鸣人:产生一模一样的本体?是什么意思呀? 不明白
依鲁卡:简单的说就是,一个鸣人变成两个鸣人,两个鸣人同时找别人打架
鸣人: 两个鸣人,这个听起来真酷,我要学,我要学。
依鲁卡:好
依鲁卡: 下面我就把口诀教给你

下面进入GO 课堂
在go 语言中,只需要在函数前加一个关键字go, 就会产生一个协程(协程是go 语言维护的轻量级线程),新产生的协程会和原来的协程分开独立执行。
下面是产生新的新协程的例子

  1. package main
  2. func main() {
  3. go nwRoutine()
  4. }
  5. func nwRoutine() {
  6. }

上面的例子中,go nwRoutine() 会产生一个新的协程, 函数nwRoutine()会在新的协程中和原来的协程main()并发执行

GO 课堂结束
依鲁卡: 鸣人,口诀记住了吗?
鸣人: 记住了
依鲁卡:好吧,今天就到这里吧,你回家好好练习,下课
鸣人: 好的。老师再见

第二天 ‘分身’的疑惑 - 协程无效

鸣人: 老师好
依鲁卡: 同学们好
依鲁卡: 鸣人,昨天的作业完成了吗?
鸣人: 作业? 什么作业? 现在都在减负呢,你不知道吗?
依鲁卡: 。。。
依鲁卡: 昨天教你的“分身术”你学的怎么样了?
鸣人: “分身术”,什么玩意,一点作用都没有,害的我白高兴了一天。老师就会糊弄我。
依鲁卡: 鸣。。。人。。。
依鲁卡: 好吧,你给展示一下,我看看到底是怎么个糊弄法。
鸣人: 那好,我就表演一下
下面进入GO 课堂
下面是一段go 程序,在main中输出I am Naruto, 在新建的协程中输出I am Naruto too

  1. package main
  2. import "fmt"
  3. func main() {
  4. go nwRoutine()
  5. fmt.Println("I am Naruto")
  6. }
  7. func nwRoutine() {
  8. fmt.Println("I am Naruto too")
  9. }

运行上述程序,输出

  1. output : I am Naruto

没有输出 I am Naruto too

GO 课堂结束
鸣人: 老师你看,根本就分身不了
依鲁卡: 哈。。。哈。。。
鸣人: 还好意思笑。。。
依鲁卡: 鸣人,我给你说,不是忍术的问题,是你查卡拉控制的问题。你查卡拉发动忍术时,消失的太快,没有办法形成“分身”。
依鲁卡: 鸣人, 我告诉你个方法, 你可以这样做。。。
下面进入GO 课堂
在main 函数中增加 sleep()

  1. package main
  2. import "fmt"
  3. import "time"
  4. func main() {
  5. go nwRoutine()
  6. fmt.Println("I am Naruto")
  7. time.Sleep(3*time.Second)
  8. }
  9. func nwRoutine() {
  10. fmt.Println("I am Naruto too")
  11. }

运行上述程序,输出

  1. output : I am Naruto
  2. output : I am Naruto too

GO 课堂结束
鸣人: 天哪,我的分身终于出现了
依鲁卡: 呵呵
依鲁卡:鸣人,回家好好练习吧,你的查卡拉越大,你生成的分身越多。你的查卡拉控制的越好,你的分身帮助你做的事情越好
依鲁卡:好吧,今天就到这里吧,下课
鸣人: 好的。老师再见

第三天 失控 - 协程控制

快上课了,依鲁卡提前走进了教室,教室里空无一人,依鲁卡静静的等待着同学们的到来
学生A: 慌慌张张的跑进教室,看见依鲁卡,哭着说,鸣人在我出家门的时候欺负我
学生B: 老师,我在吃饭的时候,鸣人抢走了我的饭
学生C: 老师,鸣人把我的玩具抢走了
。。。
学生D: 不好了,老师,门口来了十个鸣人。。。
学生E: 出大事了,出大事了,现在我们的木叶村到处都是鸣人了
依鲁卡: 大吃一惊,不好,鸣人的滥用分身术,一定是失控暴走了
依鲁卡: 大家都安静,都呆在教室里,不要出去,我去制服鸣人去
经过一番战斗,鸣人的分身都消失了,鸣人累的一动不动的躺在床上
三天后。。。
鸣人:缓缓的睁开眼,进入眼帘的是依鲁卡, 老师,我怎么了?
依鲁卡:鸣人,你滥用分身术,消耗了你太多的查卡拉,如果不是即时制止你的话,你有可能已经精尽人亡了
鸣人:啊,分身术这么烂
依鲁卡:不是分身术的问题,是你控制的问题,任何一个好的东西,都应该学会控制,如果滥用的话,会有严重的后果
鸣人:是这样呀,那我该怎么办呀?
依鲁卡:好,今天我就教你如何控制你的分身术
下面进入GO 课堂
下面是使用协程的一种常用方式

  1. package main
  2. import "fmt"
  3. func main() {
  4. nwServer()
  5. fmt.Println("I am Naruto")
  6. }
  7. func nwServer() {
  8. for {
  9. go nwRoutine()
  10. }
  11. }
  12. func nwRoutine() {
  13. fmt.Println("I am Naruto too")
  14. }

上一个方案会产生无数的协程,直到我们的资源被耗尽,应该使用下面的方式

  1. package main
  2. import "fmt"
  3. const WORKER_NUM = 10
  4. func main() {
  5. nwServer()
  6. fmt.Println("I am Naruto")
  7. }
  8. func nwServer() {
  9. for i := 0; i < WORKER_NUM; i ++ {
  10. go nwRoutine()
  11. }
  12. }
  13. func nwRoutine() {
  14. fmt.Println("I am Naruto too")
  15. }

通过设置 WORKER_NUM 的值来控制可以产生的协程的数量,防止协程的暴走

GO 课堂结束
依鲁卡:从现在开始,你使用分身术,只可以产生三个分身,知道了吗?
鸣人:知道了
依鲁卡:好吧,你好好休息吧,我回学校了
鸣人:老师再见

第四天 ‘分身’沟通 - 协程通讯

上课铃声响了,依鲁卡慢慢的走进了教室,环顾了教室,看到同学们都到了,突然,一个人从座位上站了起来
鸣人:老师,分身术我已经完全掌握了,你看。。。(出现了三个鸣人)
鸣人:但我觉得分身术一点用处都没有
依鲁卡:为什么这么说呢?
鸣人:老师你看,虽然我有分身,但我的分身都不听我的指挥。这样不是等于没有吗?
依鲁卡:不听你指挥指的是什么呢?
鸣人:我给你展示一下,分身一,你去给我看看拉面店的老板在吗?
很长时间过去了,还是不见分身的踪影
鸣人:老师你看,我让分身一给我办点事,却一个结果都得不到
依鲁卡: 呵呵,鸣人,看来你已经开始思考了。不错,有进步
依鲁卡:看在你这么大的进步的事情上,今天我再教你点新东西
鸣人: 好呀,好呀,又有新东西学了
依鲁卡:分身术一个重要的秘诀是:本体和分身之间,分身和分身之间要进行沟通,只有沟通,他们才能协同的工作
鸣人: 沟通?如何沟通呢?
依鲁卡: 箱子
鸣人:箱子?什么箱子
依鲁卡:你准备一个箱子,本体和分身做事的结果就放到箱子里。这样通过查看箱子,你就能知道结果了
鸣人:听着好像有点道理呀
依鲁卡:是的,其实。。。

下面进入GO 课堂
在go 语言中,协程之间的通信,要使用基于消息的传递,而不要使用共享变量的方式。这种基于消息传递的方式,在go 中就是channel。 也就是说,在go 中,要“以通信来共享内存,而不要以共享内存来通信”
go 的channel 也可以来实现同步,因为channel可以产生阻塞,也就是说, 向channel中写入数据通常会导致程序的阻塞,直到有其他协程从这个channel中读取数据; 反之,亦可
下面是channel的例子

  1. package main
  2. import "fmt"
  3. var box_channel = make(chan int)
  4. func main() {
  5. go nwRoutine()
  6. result := <- box_channel
  7. if result == 1 {
  8. fmt.Println("I am Naruto")
  9. }
  10. }
  11. func nwRoutine() {
  12. fmt.Println("I am Naruto too")
  13. box_channel <- 1
  14. }

运行上述程序,输出

  1. output : I am Naruto too
  2. output : I am Naruto

看这个例子,已经不用使用sleep()函数了

GO 课堂结束
鸣人:我终于明白了
依鲁卡:鸣人,你用这个方法试一下上面的事情
鸣人:好的
鸣人:果然好使,同学们,大家快来吃拉面呀
依鲁卡:鸣人。。。

第五天 多重沟通 - channel 组

在一个风高月黑的夜晚,鸣人在床上辗转反侧,怎么也睡不着,他心中有一个疑问,一个很大很大的疑问,如果这个疑问得不到解答,我想他会失眠到天亮的。
突然,鸣人出床上蹦起来,一溜烟的冲到外面去了。他要去哪里?
鸣人来到依鲁卡家门外,胆战心惊的敲打着门,
屋里的灯亮了,传来了依鲁卡老师的声音
依鲁卡: 谁呀?
鸣人:依鲁卡老师,是我, 鸣人
依鲁卡: 是鸣人呀,这么晚了,你不去睡觉,找我有什么事情吗?
鸣人:依鲁卡老师,我心中有一个疑惑,想请教一下
依鲁卡:等到明天吧,我在课堂上给你解答, 你看好吗?
鸣人:不行呀,如果这个疑惑得不到解答,我今晚就睡不着觉了
依鲁卡: 。。。 那好吧,你就进来吧。
鸣人:依鲁卡老师,“分身术”有一个天大的弊端
依鲁卡:什么天大的弊端?说来听听
鸣人:“分身术”如果要使用通信的话,就等于“分身”失效了.
依鲁卡: 为什么怎么说呢?
鸣人: 你想呀,比如我要使用“分身”来买拉面,我派一个分身去,我的本体就不得不在实时的看着箱子,只有看到箱子里有了拉面了,我的本体才能做第二件事情,这样的话,不等于我只能做一件事吗?如果我想同时做两件事或者多件事的话,我该怎么办呢,就比如说,我想同时买拉面和烤串?
依鲁卡:呵呵,鸣人,很好,你想的没错
依鲁卡: 今天我再教你一个秘诀,解决你的疑惑

下面进入GO 课堂
在go语言中,channel初始化时是可以设置长度, 比如

  1. var box_channel = make(chan int 2)

这样当有 2 个协程正在处理请求时,不会发生阻塞,再有请求过来的话,才会出现被阻塞的情况

下面的代码,会出现同时有两个协程无阻塞的工作

  1. package main
  2. import "fmt"
  3. import "time"
  4. var box_channel = make(chan int, 2)
  5. const WORKER_NUM = 10
  6. func main() {
  7. go nwServer()
  8. time.Sleep(5 * time.Second)
  9. result := <-box_channel
  10. if result == 1 {
  11. fmt.Println("I am Naruto")
  12. }
  13. }
  14. func nwServer() {
  15. for i := 0; i < WORKER_NUM; i++ {
  16. go nwRoutine()
  17. }
  18. }
  19. func nwRoutine() {
  20. box_channel <- 1
  21. fmt.Println("I am Naruto too")
  22. }

运行上述程序,输出

  1. output : I am Naruto too
  2. output : I am Naruto too
  3. output : I am Naruto

下面的代码,所有协程无阻塞的工作

  1. package main
  2. import "fmt"
  3. var box_channel = make(chan int, 2)
  4. const WORKER_NUM = 10
  5. func main() {
  6. go nwServer()
  7. for result := range box_channel {
  8. fmt.Println(result)
  9. if result == 9 {
  10. close(box_channel)
  11. }
  12. }
  13. fmt.Println("I am Naruto")
  14. }
  15. func nwServer() {
  16. for i := 0; i < WORKER_NUM; i++ {
  17. go nwRoutine(i)
  18. }
  19. }
  20. func nwRoutine(i int) {
  21. box_channel <- i
  22. fmt.Println("I am Naruto too")
  23. }

GO 课堂结束

鸣人: 哦,原来可以这样呀, 我可以准备多个箱子,太简单了
依鲁卡: 呵呵,说了这么多,肚子都有点饿了,鸣人,有你的新的技巧去买一碗拉面和一碗刀削面吧
鸣人: 好的,马上让分身去完成

第六天 时限 - 超时机制

鸣人:最近比较烦,比较烦。。。
依鲁卡:谁在课堂上唱歌?
鸣人:是我,老师
依鲁卡:鸣人,你怎么回事?为什么要在课堂上唱歌呀?
鸣人:老师,我最近心情好差呀
依鲁卡:为什么?
鸣人:我的“分身”术又遇到瓶颈了
依鲁卡:什么瓶颈? 说来听听
鸣人:不说了,说多了都是泪呀
依鲁卡:呵呵,你不说,我怎么帮你呢
鸣人:好吧,那我就说说我的瓶颈
鸣人:本来认为分身可以高效的生活,但我发现我的生活一点都高效不起来
鸣人:记得有一次,我让我的分身去帮我买拉面,半天都没有回来,快把我饿死了。最后我不得不自己亲自去看看,发现原来拉面店没有开门,我的分身在傻乎乎的等呢。马尼,这不是饿死我的节奏吗?
鸣人:如果能给我的分身指定时间就好了
依鲁卡:说的好,今天我就教你如何设置时限

下面进入GO 课堂
Go 语言没有提供直接的超时处理机制,但可以利用select 机制,因为select的特点是只要其中一个case 已完成,程序就会继续向下执行,而不会考虑其他case的情况

  1. package main
  2. import "fmt"
  3. import "time"
  4. var box_channel = make(chan int)
  5. func main() {
  6. go nwRoutine()
  7. select {
  8. case <- box_channel:
  9. fmt.Println("I am Naruto")
  10. case <- time.After(time.Second):
  11. fmt.Println("Time Out")
  12. }
  13. }
  14. func nwRoutine() {
  15. fmt.Println("I am Naruto too")
  16. time.Sleep(3*time.Second)
  17. box_channel <- 1
  18. }

GO 课堂结束
鸣人: 太好了
鸣人:学会了这个,我再也不用担心迷失了

第七天 广播 - 广播机制

既然有了超时机制,那也需要一种机制来告知其他goroutine结束手上正在做的事情并退出。很明显,还是需要利用channel来进行交流,第一个想到的肯定就是向某一个chan发送一个struct即可。比如执行任务的goroutine在参数中,增加一个chan struct{}类型的参数,当接收到该channel的消息时,就退出任务。但是,还需要解决两个问题:

怎样能在执行任务的同时去接收这个消息呢?
如何通知所有的goroutine?
对于第一个问题,比较优雅的作法是:使用另外一个channel作为函数d输出,再加上select,就可以一边输出结果,一边接收退出信号了。

另一方面,对于同时有未知数目个执行goroutine的情况,一次次调用done <-struct{}{},显然无法实现。这时候,就会用到golang对于channel的tricky用法:当关闭一个channel时,所有因为接收该channel而阻塞的语句会立即返回。示例代码如下:

  1. // 执行方
  2. func doTask(done <-chan struct{}, tasks <-chan Task) (chan Result) {
  3. out := make(chan Result)
  4. go func() {
  5. // close 是为了让调用方的range能够正常退出
  6. defer close(out)
  7. for t := range tasks {
  8. select {
  9. case result <-f(task):
  10. case <-done:
  11. return
  12. }
  13. }
  14. }()
  15. return out
  16. }
  17. // 调用方
  18. func Process(tasks <-chan Task, num int) {
  19. done := make(chan struct{})
  20. out := doTask(done, tasks)
  21. go func() {
  22. <- time.After(MAX_TIME)
  23. //done <-struct{}{}
  24. //通知所有的执行goroutine退出
  25. close(done)
  26. }()
  27. // 因为goroutine执行完毕,或者超时,导致out被close,range退出
  28. for res := range out {
  29. fmt.Println(res)
  30. //...
  31. }
  32. }

协程实践

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注