Golang中的的goroutine、channel和死锁

goroutine

Go语言中有个概念叫做goroutine, goroutine的本质是协程,是实现并行计算的核心。
以下程序串行地执行两次loop函数:

package main

import (
    "fmt"
)

func loop() {
    for i := 0; i < 10; i++ {
        fmt.Printf("%d ", i)
    }
}

func main() {
    loop()
    loop()
}

毫无疑问会有如下输出

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

下面在一个goroutine里跑一次loop方法,使用关键字go来定义并启动一个goroutine:

func main() {
    go loop()
    loop()
}

变成了以下输出

0 1 2 3 4 5 6 7 8 9

为什么只输出了一次呢?因为goroutine还没来得及执行loop的时候main函数已经退出了。下面让main函数等待一下

func main() {
    go loop()
    loop()
    time.Sleep(time.Second) // 停顿一秒
}

这次输出了两次,目的达到了。但是使用等待的方法并不好,如果goroutine能阻塞住main函数并且在执行结束后主动释放就好了。

channel

channel有点类似于Unix中的管道,它在goroutine同步与通信中,有着起承转合的作用,channel分无缓冲和有缓冲两种,这里先使用无缓冲channel来讲一下阻塞的问题。
使用make来创建一个channel

var channel chan int = make(chan int)

channel := make(chan int)

那如何向channel发消息和从channel取消息呢?

package main

import (
    "fmt"
)

func main() {
    var msgChan chan string = make(chan string)
    go func(message string) {
        msgChan <- message // 存消息
    }("hello")

    fmt.Println(<-msgChan) // 取消息
}

无缓冲channel在取消息和发消息的时候都会挂起当前的goroutine,除非另一端已经准备好。

package main

var ch chan int = make(chan int)

func foo() {
    ch <- 0 // 向ch中加数据,如果没有其他goroutine来取走这个数据,那么挂起foo, 直到main函数把0这个数据拿走
}

func main() {
    go foo()
    <-ch // 从ch取数据,如果ch中还没放数据,那就挂起main线,直到foo函数中放数据为止
}

既然channel可以阻塞当前的goroutine, 如何让goroutine告诉主线执行完毕了呢? 显然使用channel即可。

package main

import (
    "fmt"
)

var complete chan int = make(chan int)

func loop() {
    for i := 0; i < 10; i++ {
        fmt.Printf("%d ", i)
    }
    complete <- 0 // 执行完毕了,发个消息
}

func main() {
    go loop()
    <-complete // 直到goroutine跑完, 取到消息, 否则main在此阻塞住
}

无缓冲channel不会存储数据,只负责数据的流通,从无缓冲channel取数据,必须要有数据流进来才可以,否则阻塞,数据流入无缓冲channel, 如果没有其他goroutine来拿走这个数据也会阻塞。
如果向无数据流出的channel加数据或者向无数据流入的channel取数据都会引起死锁。

死锁

一个死锁的例子:

func main() {
    ch := make(chan int)
    <- ch // 阻塞main goroutine, ch被锁
}

执行这个程序会报如下错误

fatal error: all goroutines are asleep - deadlock!

所谓死锁就是所有的线程或进程都在等待资源的释放。上面的程序中,只有一个goroutine, 当向channel里面加数据的话就会锁死channel并且阻塞当前 goroutine。以下有几个死锁的例子:

func main() {
    ch := make(chan int)
    ch <- 1 // 1流入ch,不被取走就会阻塞
    fmt.Println("This line code wont run") //在此行执行之前Go就会报死锁
}
var ch1 chan int = make(chan int)
var ch2 chan int = make(chan int)

func say(s string) {
    fmt.Println(s)
    ch1 <- <- ch2 // ch1 等待 ch2流出的数据
}

func main() {
    go say("hello")
    <- ch1  // 堵塞主线
}

其中主线等ch1中的数据流出,ch1等ch2的数据流出,但是ch2等待数据流入,两个goroutine都在等,也就是死锁。

其实,总结来看,为什么会死锁?非缓channel如果发生了流入无流出或者流出无流入,都会导致了死锁。所以下面的示例一定死锁:

c, quit := make(chan int), make(chan int)

go func() {
   c <- 1  // c的数据没有被其他goroutine读取走,堵塞当前goroutine
   quit <- 0 // quit始终没有办法写入数据
}()

<- quit // quit 等待数据的写

仔细分析的话,是由于:主线等待quit的数据流出,quit等待数据写入,而func被c通道堵塞,所有goroutine都在等,所以死锁。但是,是否所有不成对向channel存取数据的情况都是死锁?如下是个反例:

func main() {
    c := make(chan int)

    go func() {
       c <- 1
    }()
}

程序正常退出了,原因是main又没等待其它goroutine,自己先跑完了, 所以没有数据流入c,一共执行了一个goroutine, 并且没有发生阻塞,所以没有死锁产生。
那么死锁的解决办法呢?最简单的,把没取走的数据取走,没放入的数据放入。
具体来讲,就死锁例子3中的情况,可以这么避免死锁:

c, quit := make(chan int), make(chan int)

go func() {
    c <- 1
    quit <- 0
}()

<- c // 取走c的数据!
<-quit

无缓冲channel的数据进出顺序

无缓冲channel从不存储数据,流入的数据必须要流出才可以。观察以下的程序:

package main

import (
    "fmt"
)

var ch chan int = make(chan int)

func foo(id int) { //id: 这个routine的标号
    ch <- id
}

func main() {
    // 开启5个routine
    for i := 0; i < 5; i++ {
        go foo(i)
    }

    // 取出channel中的数据
    for i := 0; i < 5; i++ {
        fmt.Print(<-ch)
    }
}

开了5个goroutine,然后又依次取数据。每次执行输出的数据顺序都是不一样的,可以看出无缓冲channel的数据是先到先出。

buffered channel

buffered channel不仅可以流通数据,还可以缓存数据。它是有容量的,存入一个数据的话 , 可以先放在channel里,不必阻塞当前线而等待该数据取走。当buffered channel达到满的状态的时候,就会表现出阻塞了,因为这时再也不能承载更多的数据了。
在声明的时候,使用第二个参数来指明它的容量(默认为0,即无缓冲):

var ch chan int = make(chan int, 2)

如下的例子,ch可以无缓冲的流入3个元素:

func main() {
    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3
}

如果再试图流入一个数据的话,ch会阻塞main线, 报死锁。也就是说,buffered channel会在满容量的时候加锁。
其实,buffered channel是先进先出的,可以把buffered channel看作为一个线程安全的队列

func main() {
    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3

    fmt.Println(<-ch) // 1
    fmt.Println(<-ch) // 2
    fmt.Println(<-ch) // 3
}

上面的代码一个一个地去读简直太费事了,可以使用range来读:

func main() {
    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3

    for v := range ch {
        fmt.Println(v)
    }
}

如果执行了上面的代码会报死锁错误,原因是range不等到ch关闭是不会结束读取的。也就是如果 buffered channel干涸了,那么range就会阻塞当前goroutine, 所以死锁。
怎么避免这种情呢?比较容易想到的是读到channel为空的时候就结束读取

ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
for v := range ch {
    fmt.Println(v)
    if len(ch) <= 0 { // 如果现有数据量为0,跳出循环
        break
    }
}

以上的方法是可以正常输出的,但是注意检查channel大小的方法不能在存取都在发生的时候用于取出所有数据,这个例子 是因为只在ch中存了数据,现在一个一个往外取,大小是递减的。另一个方式是显式地关闭channel

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

// 显式地关闭channel
close(ch)

for v := range ch {
    fmt.Println(v)
}

被关闭的channel会禁止数据流入, 是只读的。仍然可以从关闭的channel中取出数据,但是不能再写入数据了。

36nu 分享编程知识及经验

已有账号?立即登录
微信公众号
关注36nu微信公众账号
获取最新编程知识及经验