golang goroutine协程运行机制及使用详解

Go(又称Golang)是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。Go于2009年正式推出,国内各大互联网公司都有使用,尤其是七牛云,基本都是golang写的,
传闻Go是为并发而生的语言,运行速度仅比c c++慢一点,内置协程(轻量级的线程),说白了协程还是运行在一个线程上,由调度器来调度线程该运行哪个协程,也就是类似于模拟了一个操作系统调度线程,我们也知道,其实多线程说白了也是轮流占用cpu,其实还是顺序执行的,协程也是一样,他也是轮流获取执行机会,只不过他获取的是线程,但是如果cpu是多核的话,多线程就能真正意义上的实现并发同时,如果GO执行过程中有多个线程的话,协程也能实现真正意义上的并发执行,所以,最理想的情况,根据cpu核数开辟对应数量的线程,通过这些线程,来为协程提供执行环境
当我们在开发网络应用程序时,遇到的瓶颈总是在io上,由此出现了多进程,多线程,异步io的解决方案,其中异步io最为优秀,因为他们在不占用过多的资源情况下完成高性能io操作,但是异步io会导致一个问题,那就是回调地狱,node js之前深受诟病的地方就在于此,后来出现了async await这种方案,真正的实现了同步式的写异步,其实Go的协程也是这样,有人把goroutine叫做纤程,认为node js的async await才是真正的协程,对此我不做评价,关于goroutine的运行机制本文不讲,大家可以看这篇博文,讲的很生动,本文主要对goroutine的使用进行讲解,如果大家熟悉node js的async await或者c#的async(其实node js就是学习的c#的async await),可以来对比一下两者在使用上的不同,从而对协程纤程的概念产生进一步的了解
在golang中开辟一个协程非常简单,只需要一个go关键字

package main
  
import (
        "fmt"
        "time"
)


func main(){
        for i := 0;i<10;i++{
                go func(i int){
                        for{
                                fmt.Printf("%d",i);
                           }
                }(i)
        }
        time.Sleep(time.Millisecond);
}

打印结果

5551600088800499999991117777777742222220000044444444888888888999
9666665111177777777777777777777777777333333333333333399999999999
999999999999999999999999999999444442224444444488888888222222222
20888886666666655555555555444011111111111111000000000999999555555
5554444444000077777666666311111197777778888222277777753333444444
9999997777772222000077774444444444444444444

可以看到,完全是随机的,打印哪个取决于调度器对协程的调度,
goroutine相比于线程,有个特点,那就是非抢占式,如果一个协程占据了线程,不主动释放或者没有发生阻塞的话,那么永远不会交出线程的控制权,我们举个例子来验证下

package main
  
import (
       "time"
)
func main(){
        for i := 0;i<10;i++{
                go func(i int){
                        for{
                                i++                                
                          }
                }(i)
        }
        time.Sleep(time.Millisecond);
}

这段程序在执行后,永远不会退出,并且占满了cpu,原因就是goroutine中,一直在执行i++,没有释放,而一直占用线程,当四个线程占满之后,其他的所有goroutine都没有执行的机会了,所以本该一秒钟后就退出的程序一直没有退出,cpu满载再跑,但是为什么前面例子的Printf没有发生这种情况呢?是因为Printf其实是个io操作,io操作会阻塞,阻塞的时候goroutine就会自动的释放出对线程的占有,所以其他的goroutine才有执行的机会,除了io阻塞,golang还提供了一个api,让我们可以手动交出控制权,那就是Gosched(),当我们调用这个方法时,goroutine就会主动释放出对线程的控制权

package main
  
import (
       "time"
      "runtime"
)
func main(){
        for i := 0;i<10;i++{
                go func(i int){
                        for{
                                i++;
                                runtime.Gosched();                                
                          }
                }(i)
        }
        time.Sleep(time.Millisecond);
}

修改之后,一秒钟之后,代码正常退出
常见的触发goroutine切换,有一下几种情况

1、I/O,select

2、channel

3、等待锁

4、函数调用(是一个切换的机会,是否会切换由调度器决定)

5、runtime.Gosched()

说完了goroutine的基本用法,接下来我们说一下goroutine之间的通信,Go中通信的理念是“不要通过共享数据来通信,而是通过通信来共享数据“,Go中实现通信主要通过channel,它类似于unix shell中的双向管道,可以接受和发送数据,
我们来看个例子,

package main
  
import(
        "fmt"
        "time"
)

func main(){
        c := make(chan int)
        go func(){
           for{
                n := <-c;
                fmt.Printf("%d",n)
              }
        }()

        c <- 1;
        c <- 2;
        time.Sleep(time.Millisecond);


}

打印结果为12,我们通过make来创建channel类型,并指明存放的数据类型,通过 <-来接收和发送数据,c <- 1为向channel c发送数据1,n := <-c;表示从channel c接收数据,默认情况下,发送数据和接收数据都是阻塞的,这很容易让我们写出同步的代码,因为阻塞,所以会很容易发生goroutine的切换,并且,数据被发送后一定要被接收,不然会一直阻塞下去,程序会报错退出,
本例中,首先向c发送数据1,main goroutine阻塞,执行开辟的协程,从而读到数据,打印数据,然后main协程阻塞完成,向c发送第二个数据2,开辟的协程还在阻塞读取数据,成功读取到数据2时,打印2,一秒钟后,主函数退出,所有goroutine销毁,程序退出

我们仔细看这份代码,其实有个问题,在开辟的goroutine中,我们一直再循环阻塞的读取c中的数据,并不知道c什么时候写入完成,不再写入,如果c不再写入我们完全可以销毁这个goroutine,不必占有资源,通过close api我们可以完成这一任务,

package main
  
import (
        "fmt"
        "time"
)

func main(){
        c := make(chan int);
        go func(){
            for{
                p,ok := <-c;
                if(!ok){
                        fmt.Printf("jieshu");
                        return
                }
                fmt.Printf("%d",p);
               }
        }()
        for i := 0;i<10;i++{
                c<-i
        }
        close(c);
}

当我们对channel写入完成后,可以调用close方法来显式的告诉接收方对channel的写入已经完毕,这是,在接收的时候我们可以根据接收的第二个值,一个boolean值来判断是否完成写入,如果为false的话,表示此channel已经关闭,我们没有必要继续对channel进行阻塞的读,
除了判断第二个boolean参数,go还提供了range来对channel进行循环读取,当channel被关闭时就会退出循环,

package main
  
import (
        "fmt"
        "time"
)

func main(){
        c := make(chan int);
        go func(){
        //    for{
        //      p,ok := <-c;
        //      if(!ok){
        //              fmt.Printf("jieshu");
        //              return
        //      }
                for p := range c{
                        fmt.Printf("%d",p);
                }
                fmt.Printf("jieshu");
        //   }
        }()
        for i := 0;i<10;i++{
               c<-i
        }
        close(c);
        time.Sleep(time.Millisecond);

}

两种方式打印的都是123456789jieshu

另外,通过Buffered Channels我们可以创建带缓存的channel,使用方法为创建channel时传入第二个参数,指明缓存的数量,

package main

import "fmt"

func main() {
    c := make(chan int, 2)//修改2为1就报错,修改2为3可以正常运行
    c <- 1
    c <- 2
    fmt.Println(<-c)
    fmt.Println(<-c)
}

例子中,我们创建channel时,传入参数2,便可以存储两个两个数据,前两个数据的写入可以无阻塞的,不需要等待数据被读出,如果我们连续写入三个数据,就会报错,阻塞在第三个数据的写入出无法进行下一步

最后,我们说一下select,这个和操作系统io模型中的select很像,先执行先到达的channel我们看个例子

package main
  
import (
        "fmt"
        "time"
)

func main(){

        c := make(chan int);
        c2:= make(chan int);

        go func(){
         for{
                select{
                        case p := <- c : fmt.Printf("c:%d\n",p);
                        case p2:= <- c2: fmt.Printf("c2:%d\n",p2);
                }
            }
        }()

        for i :=0;i<10;i++{
                go func(i int){
                        c <- i
                }(i)
                go func (i int){
                        c2 <-i
                }(i)
        }
        time.Sleep(5*time.Millisecond);
}

打印结果为

c:0
c2:1
c:1
c:2
c2:0
c:3
c:4
c:5
c:7
c2:2
c:6
c:8
c:9
c2:3
c2:5
c2:4
c2:6
c2:7
c2:8
c2:9

可以看到,c和c2的接收完全是随机的,谁先接收到执行谁的回调,当然这不仅限于接收,发送数据时也可以使用select函数,另外,和switch语句一样,golang中的select函数也支持设置default,当没有接收到值的时候就会执行default回调,如果没有设置default,就会阻塞在select函数处,直到某一个发送或者接收完成。

golang中 goroutine的基本使用就是这些,大家可以根据上面goroutine运行机制的文章和本文一起来体会golang的运行过程。

补充一个runtime包的几个处理函数

  • Goexit
    退出当前执行的goroutine,但是defer函数还会继续调用
  • Gosched
    让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。
  • NumCPU
    返回 CPU 核数量
  • NumGoroutine
    返回正在执行和排队的任务总数
  • GOMAXPROCS
    用来设置可以并行计算的CPU核数的最大值,并返回之前的值。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,692评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,482评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,995评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,223评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,245评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,208评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,091评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,929评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,346评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,570评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,739评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,437评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,037评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,677评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,833评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,760评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,647评论 2 354

推荐阅读更多精彩内容