Go学习笔记-初学协程

协程

学习go也有一段时间了,这里讲一下自己对go协程的使用理解。
go很多人都知道,毕竟有个好爹Google,提起go和其它语言最大区别莫过于 goroutine,也就是go的协程,先来一个demo:

package main

func say(s string) {
    for i := 0; i < 5; i++ {
        println(s)
    }
}

func main() {
    go say("Hello")
    go say("World")
}

go 启动协程的方式就是使用关键字 go,后面一般接一个函数或者类似下面的匿名函数的写法

go func() {
    for i := 0; i < 5; i++ {
        println(i)
    }
}()

当然如果你运行上面第一段代码,你会发现什么结果都没有,what???

这至少说明你代码写的没问题,当你使用go启动协程之后,这2个函数就被切换到协程里面执行了,但是这时候主线程结束了,这2个协程还没来得及执行就挂了!
聪明的小伙伴会想到,那我主线程先睡眠1s等一等?Yes, 在main代码块最后一行加入:

time.Sleep(time.Second*1) # 表示睡眠1s

你会发现可以打印出5个Hello 和 5个World,多次运行你会发现Hello 和 World 的顺序不是固定的,这进一步说明了一个问题,那就是多个协程是同时执行的
不过睡眠这种做法肯定是不靠谱的,go 自带一个WaitGroup可以解决这个问题, 代码如下:

package main

import (
    "sync"
)

var wg sync.WaitGroup

func say(s string) {
    for i := 0; i < 5; i++ {
        println(s)
    }
    wg.Done()
}

func main() {
    wg.Add(2)
    
    go say("Hello")
    go say("World")
    
    wg.Wait()
}

简单说明一下用法,var 是声明了一个全局变量 wg,类型是sync.WaitGroup,wg.add(2) 是说我有2个goroutine需要执行,
wg.Done 相当于 wg.Add(-1) 意思就是我这个协程执行完了。wg.Wait() 就是告诉主线程要等一下,等他们2个都执行完再退出。

举个例子,你有一个需求是从3个库取不同的数据汇总处理,同步代码的写法就是查3次库,但是这3次查询必须按顺序执行,大部分编程语言的代码执行顺序都是从上到下,假如一个
查询耗时1s,3个查询就是3s,但是使用协程你可以让这3个查询同时进行,也就是1s就可以搞定(前提是数据库跟得上)。还有一个更有实际用途的例子就是用来写爬虫。

不过为了更好的使用协程,你可能还得了解一下管道 Chanel,go 里面的管道是协程之间通信的渠道,上面的例子里面我们是直接打印出来结果,假如现在的需求是把输出结果返回到主线程呢?

package main

import (
    "sync"
)

var wg sync.WaitGroup

func say(s string, c chan string) {
    for i := 0; i < 5; i++ {
        c <- s
    }
    wg.Done()
}

func main() {
    wg.Add(2)

    ch := make(chan string) // 实例化一个管道

    go say("Hello", ch)
    go say("World", ch)

    for {
        println(<-ch) //循环从管道取数据
    }

    wg.Wait()
}

简单说明一下,这里就是实例化了一个管道,go启动的协程同时向这个2个管道输出数据,主线程使用了一个for循环从管道里面取数据,其实就是一个生产者和消费者模式,和redis队列有点像

值得一说的是 World 和 Hello 进入管道的顺序是不固定的,可能大家实验的时候发现好像是固定的,那是因为电脑跑的太快了,你把循环数据放大,或者在里面加个睡眠再看看

但是这个程序是有bug的,在程序的运行的最后会输出这样的结果:

fatal error: all goroutines are asleep - deadlock! 

报错信息的提示意思是所有的协程都睡眠了,程序监测到死锁!为什么会这样呢?我是这样理解的,go的管道默认是阻塞的(假如你不设置缓存的话),你那边放一个,我这头才能取一个,
如果你那边放了东西这边没人取,程序就会一直等下去,死锁了,同时,如果那边没人放东西,你这边取也取不到,也会发生死锁!

如何解决这个问题呢?标准的做法是主动关闭管道,或者你知道你应该什么时候关闭管道, 当然你结束程序管道自然也会关掉!针对上面的演示代码,可以这样写:

i := 1
for {
    str := <- ch
    println(str)

    if i >= 10{
        close(ch)
        break
    }
    i++
}

因为我们明确知道总共会输出10个单词,所以这里简单做了一个判断,大于10就关闭管道退出for循环,就不会报错了!下面是一个利用select从管道取数据的例子:

package main

import (
    "strconv"
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan int)
    ch2 := make(chan string)
    go pump1(ch1)
    go pump2(ch2)
    go suck(ch1, ch2)
    time.Sleep(time.Duration(time.Second*30))
}

func pump1(ch chan int) {
    for i := 0; ; i++ {
        ch <- i * 2
        time.Sleep(time.Duration(time.Second))
    }
}

func pump2(ch chan string) {
    for i := 0; ; i++ {
        ch <- strconv.Itoa(i+5)
        time.Sleep(time.Duration(time.Second))
    }
}

func suck(ch1 chan int, ch2 chan string) {
    chRate := time.Tick(time.Duration(time.Second*5)) // 定时器
    for {
        select {
        case v := <-ch1:
            fmt.Printf("Received on channel 1: %d\n", v)
        case v := <-ch2:
            fmt.Printf("Received on channel 2: %s\n", v)
        case <-chRate:
            fmt.Printf("Log log...\n")
        }
    }
}

输出结果如下:

Received on channel 1: 0
Received on channel 2: 5
Received on channel 2: 6
Received on channel 1: 2
Received on channel 1: 4
Received on channel 2: 7
Received on channel 1: 6
Received on channel 2: 8
Received on channel 2: 9
Received on channel 1: 8
Log log...
Received on channel 2: 10
Received on channel 1: 10
Received on channel 1: 12
Received on channel 2: 11
Received on channel 2: 12
Received on channel 1: 14

这个程序建立了2个管道一个传输int,一个传输string,同时启动了3个协程,前2个协程非常简单,就是每隔1s向管道输出数据,第三个协程是不停的从管道取数据,
和之前的例子不一样的地方是,pump1 和 pump2是2个不同的管道,通过select可以实现在不同管道之间切换,哪个管道有数据就从哪个管道里面取数据,如果都没数据就等着,
还有一个定时器功能可以每隔一段时间向管道输出内容!

最后,值得一说的是,go 自带的web server性能非常强悍,主要就是因为使用了协程,对于每一个web请求,服务器都会新开一个go协程去处理,一个服务器可以轻松同时开启上万个协程。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容