A Bite of GoLang (6)

叁叁肆2018-09-19 11:23


本文来自网易云社区


作者:盛国存


8. Goroutine

8.0、Goroutine介绍

协程 Coroutine

轻量级"线程"

上面的两个特征到底是什么意思呢?下面我们通过具体的事例详细的讲述一下,

package main

import "fmt"

func main() {

    for i := 0; i < 10; i ++ {
        func(i int){
            for {
                fmt.Println("Goroutine :" , i)
            }
        }(i)
    }
}

上面这段代码有问题么? 这就是一个从 0 到 10 的调用,但是匿名函数内部没有中止条件,所以会进入一个死循环。要是我们在匿名函数前加上 go 关键字,就不是刚才的意思了,就变成并发执行这个函数。主程序继续向下跑,同时并发开了一个函数,就相当于开了一个线程,当然我们后面会继续介绍,这个叫 协程

package main

import "fmt"

func main() {

    for i := 0; i < 10; i ++ {
        go func(i int){
            for {
                fmt.Println("Goroutine :" , i)
            }
        }(i)
    }
}

我们再执行这段代码,发现什么都没有输出,这又是为什么呢?因为这个 mainfmt.Println 是并发执行的,我们还来不及print结果, main 就执行完成退出了。Go语言一旦main函数退出了,所有的Goroutine就被杀掉了。 当然要是想看到输出结果,main函数可以在最后sleep一下

package main

import (
    "fmt"
    "time"
)

func main() {

    for i := 0; i < 10; i ++ {
        go func(i int){
            for {
                fmt.Println("Goroutine :" , i)
            }
        }(i)
    }
    time.Sleep(time.Millisecond)
}

这时候就有相关的结果输出了。那我们将现在的10改成1000,又会怎样呢?当然还是可以正常输出的,熟悉操作系统的都知道正常的线程几十个上百个是没啥问题的,1000个还是有点难度的,其它语言通常使用异步IO的方式。在Go语言中我们不用管10个、100个、1000个代码还是一样的写法。


非抢占式多任务处理,由协程主动交出控制权

非抢占式多任务 这又是什么意思呢?下面我们用一个例子来解释一下

package main

import (
    "time"
    "fmt"
)

func main() {

    var a [10]int
    for i := 0; i < 10; i ++ {
        go func(i int){
            for {
                a[i] ++
            }
        }(i)
    }
    time.Sleep(time.Millisecond)
    fmt.Println(a)
}

在运行之前,可以想一下会输出什么呢? 什么也没有输出,进入了死循环。

上图是我的活动监视器的截图,因为是4核的机器,几乎全部占满了。退不出的原因是因为Goroutine a[i] 交不出控制权,没有yield出去,同时main函数也是一个goroutine,因为没人交出控制权,所以下面的sleep永远也不会执行。 那我该如何交出控制权呢?我们可以做一个IO操作可以交出控制权,当然也可以手动交出控制权

package main

import (
    "time"
    "fmt"
    "runtime"
)

func main() {

    var a [10]int
    for i := 0; i < 10; i ++ {
        go func(i int){
            for {
                a[i] ++
                runtime.Gosched()
            }
        }(i)
    }
    time.Sleep(time.Millisecond)
    fmt.Println(a)
}

只需要加上 runtime.Gosched() ,这样大家都主动让出控制权,这时候代码可以正常输出了

[321 986 890 880 831 881 919 904 861 904]

Process finished with exit code 0

如果我们把goroutine的参数 i 去掉会怎样呢? 直接的看语法上没有什么问题,就变成了一个闭包,使用外部的变量 i

package main

import (
    "time"
    "fmt"
    "runtime"
)

func main() {

    var a [10]int
    for i := 0; i < 10; i ++ {
        go func(){
            for {
                a[i] ++
                runtime.Gosched()
            }
        }()
    }
    time.Sleep(time.Millisecond)
    fmt.Println(a)
}

运行之后会出现什么问题呢?

panic: runtime error: index out of range

goroutine 6 [running]:
main.main.func1(0xc42001a0f0, 0xc42001c060)
    /Users/verton/GoLangProject/src/shengguocun.com/goroutine/route.go:15 +0x45
created by main.main
    /Users/verton/GoLangProject/src/shengguocun.com/goroutine/route.go:13 +0x95

Process finished with exit code 2

这里我们通过Go语言的 go run -race xxx.go ,执行分析一下

sheng$ go run -race route.go
==================
WARNING: DATA RACE
Read at 0x00c420092008 by goroutine 6:
  main.main.func1()
      /Users/verton/GoLangProject/src/shengguocun.com/goroutine/route.go:15 +0x54

Previous write at 0x00c420092008 by main goroutine:
  main.main()
      /Users/verton/GoLangProject/src/shengguocun.com/goroutine/route.go:12 +0x11b

Goroutine 6 (running) created at:
  main.main()
      /Users/verton/GoLangProject/src/shengguocun.com/goroutine/route.go:13 +0xf1
==================

这个地址 0x00c420092008 是谁呢,很显然就是 i ,原因是因为在最后跳出来的时候 i 会变成10,里面的 a[i] ++ 就会是a[10] ,所以出错的原因就在这。

sheng$ go run -race route.go
==================
WARNING: DATA RACE
Read at 0x00c420092008 by goroutine 6:
  main.main.func1()
      /Users/verton/GoLangProject/src/shengguocun.com/goroutine/route.go:15 +0x54

Previous write at 0x00c420092008 by main goroutine:
  main.main()
      /Users/verton/GoLangProject/src/shengguocun.com/goroutine/route.go:12 +0x11b

Goroutine 6 (running) created at:
  main.main()
      /Users/verton/GoLangProject/src/shengguocun.com/goroutine/route.go:13 +0xf1
==================

上面还剩一个的两个Goroutine读写的问题需要我们后面介绍的Channel来解决。


8.1、Go语言调度器

多个协程可能在一个或多个线程上运行

首先我们先看一张普通函数和协程的对比图

普通函数main函数和work函数都运行在一个线程里面,main函数在等work函数执行完才能执行其他的操作。可以看到普通函数 main 函数和 work 函数是单向的,但是发现协程的 main 和 work 是双向通道的,控制权可以在work也可以在main,不需要像普通函数那样等work函数执行完才交出控制权。协程中main和work可能执行在一个线程中,有可能执行在多个线程中。

上图就是Go语言的协程, 首先下面会有一个调度器,负责调度协程,有些是一个goroutine放在一个线程里面,有的是两个,有的是多个,这些我们都不需要关注。

goroutine定义


  • 任何函数只需要加上go就能送给调度器运行

  • 不需要在定义时区分是否是异步函数

  • 调度器在合适的点进行切换

  • 使用-race来检测数据访问冲突
goroutine可能的切换点
  • I/O 、select

  • channel

  • 等待锁

  • 函数调用(有时)

  • runtime.Gosched()

上述仅是参考,不能保证切换,不能保证在其他的地方不切换


9. Channel

9.0、Channel介绍

Channel


我们可以开很多个goroutine,goroutine和goroutine之间的双向通道就是channel。 首先我们先来介绍一下channel的用法

ch := make(chan int)

和其他类型类似,都是需要先创建声明

package main

import (
    "fmt"
    "time"
)

func main()  {

    ch := make(chan int)
    go func() {
        for  {
            num := <- ch
            fmt.Println(num)
        }
    }()

    ch <- 1
    ch <- 2
    time.Sleep(time.Millisecond)
}

这就是一个简单的channel示例,同时channel是一等公民,可以作为参数也可以作为返回值,那我们就用一个例子来简单的演示一下

package main

import (
    "fmt"
    "time"
)

func work(channels chan int, num int) {
    for ch := range channels {

        fmt.Println("Work ID :", num)
        fmt.Println(ch)
    }
}

func createWork(num int) chan<- int {

    ch := make(chan int)
    go work(ch, num)
    return ch
}


func main()  {

    var channels [10]chan<- int
    for i := 0; i < 10; i ++ {
        channels[i] = createWork(i)
    }

    for i := 0; i < 10; i ++ {
        channels[i] <- 'M' + i
    }
    time.Sleep(time.Millisecond)
}

输出结果为

Work ID : 3
80
Work ID : 0
77
Work ID : 1
78
Work ID : 6
Work ID : 9
83
Work ID : 4
Work ID : 5
82
86
81
Work ID : 8
85
Work ID : 2
79
Work ID : 7
84

结果为什么是乱序的呢?因为 fmt.Println 有I/O操作;上述例子,可以看到channel既可以作参数,也可以作为返回值。

Buffer Channel

ch := make(chan  int)
ch <- 1

我们要是光发送,没有接收是不行的,程序会报错,比如上述代码运行之后

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    /Users/verton/GoLangProject/src/shengguocun.com/channel/channel.go:42 +0x50

我们可以设置一个缓冲区

ch := make(chan  int, 5)
ch <- 1

缓冲区大小设置为5,只要发送不超过5个都不会报错,下面我们来演示一下buffer channel的使用

func main()  {

    channels := make(chan  int, 5)
    go func() {
        for ch := range channels {

            fmt.Println(ch)
        }
    }()
    channels <- 1
    channels <- 2
    channels <- 3
    channels <- 4
    channels <- 5
    time.Sleep(time.Millisecond)
}

结果输出正常

1
2
3
4
5

Process finished with exit code 0

比如我们确定数据结束了,可以在最后进行close;同时只能是发送方close的

func main()  {

    channels := make(chan  int, 5)
    go func() {
        for ch := range channels {

            fmt.Println(ch)
        }
    }()
    channels <- 1
    channels <- 2
    channels <- 3
    channels <- 4
    channels <- 5
    close(channels)
    time.Sleep(time.Millisecond)
}

直观地从输出结果来看,加不加close这两者是没有区别的。


相关阅读:A Bite of GoLang (1)

A Bite of GoLang (2)

A Bite of GoLang (3)

A Bite of GoLang (4)

A Bite of GoLang (5)

A Bite of GoLang (6)

A Bite of GoLang (7)

A Bite of GoLang (8)


网易云免费体验馆0成本体验20+款云产品!

更多网易研发、产品、运营经验分享请访问网易云社区