Go语言 goroutines 信道 和 死锁


Go语言 goroutine 概念  类似 线程 但更轻

串行 执行两次loop函数

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

把 loop 放在 goroutine 里 用go定义启动一个goroutine

func main() {

    go loop() // 启动一个goroutine

    loop()

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

主线跑一趟  goroutine 为什么只输出了一趟呢?

原来goroutine没来得及跑loop 主函数已经退出了

想办法阻止 main函数 过早地退出

func main() {

    go loop()

    loop()

    time.Sleep(time.Second) // 停顿一秒 main等一下 目的达到了 办法并不好

}

如果goroutine 结束时 告诉下主线Hey跑完了!”就好了  即所谓阻塞主线


信道是什么

简单说是 goroutine 间通讯的东西 类似Unix 管道(在进程间传递消息)
goroutine 间发/接收消息 是在做 goroutine 间的内存共享

用make来建立信道

var channel chan int = make(chan int)// 或

channel := make(chan int)//just  in func ,otherwise non-declaration statement outside function body

如何向 信道存/取消息

func main() {

   messages := make(chan string)

    go func(message string) {

        messages <- message // 存消息

    }("Ping!")

    fmt.Println(<-messages) // 取消息 Ping!

}默认信道的存消息和取消息都是阻塞的 (叫  无缓冲 信道 )

无缓冲 信道在取消息和存消息的时候 挂起当前 goroutine 除非另一端已经准备好

main函数和 foo 函数

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 函数中放数据为止

}

既然信道可以阻塞当前 goroutine
如何让goroutine告诉主线 执行完毕了 用一个信道来告诉主线即可

var complete chan int = make(chan int)

///complete := make(chan int)//non-declaration statement outside function body

func loop() {

    for i := 0; i < 10; i++ {

        fmt.Printf("%d ", i)

    }

    complete <- 0 // 执行完毕了 发个消息

}

func main() {

    go loop()

    <- complete // main在此阻塞住 // 直到线程跑完  取到消息

    fmt.Printf("ok" )

}

不用信道来阻塞主线的话 主线就过早跑完 loop线没有机会执行


无缓冲的信道不会存储数据 只负责数据的流通
为什么呢?

从无缓冲信道取数据 必须要有数据流进来才可以 否则当前线阻塞

数据流入无缓冲信道 如果没有其他goroutine来拿走这个数据 那么当前线阻塞

所以 测试到的无缓冲信道的大小都是0 (len(channel))


如果信道正有数据在流动 还要加入数据 或者信道干涩 一直向无数据流入的空信道取数据 就会引起死锁

何谓死锁

func main() {

    ch := make(chan int)

    <- ch // 阻塞main goroutine 信道c被锁

}

执行这个程序 报这样的错误 fatal error: all goroutines are asleep - deadlock!


所有的线程 或 进程都在等待资源的释放
只有一个 goroutine 所以当 向里面加数据或者存数据 都会锁死信道 
且阻塞当前 goroutine 就是所有goroutine(其实就main线一个) 都在等待信道的开放(没人拿走数据信道是不会开放的) 也就是死锁

只在单一的 goroutine 里操作无缓冲信道 一定死锁 比如 只在main函数里操作信道

func main() {

    ch := make(chan int)

    ch <- 1 // 1流入信道 堵塞当前线 没人取走数据信道不会打开

    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都在等 也就是死锁


为什么会死锁

Go启动的所有goroutine里 非缓冲信道 一定要

一个线里存数据

一个线里取数据

要成对才行

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都在等 所以死锁

简单来看的话 一共两个线 func线中流入c通道的数据并没有在main线中流出 肯定死锁

但是 是否 所有不成对向信道存取数据的情况都是死锁?

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


缓冲信道

即设置c有一个数据的缓冲大小

c := make(chan int, 1)

c可缓存一个数据 也就是说 放入一个数据 c并不会挂起当前线
再放一个才会挂起当前线直到第一个数据被其他goroutine取走
也就是只阻塞在容量一定的时候 不达容量不阻塞

无缓冲信道的数据进出顺序

无缓冲信道从不存储数据 流入的数据必须要流出才可以


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)

    } // 取出信道中的数据

    for i := 0; i < 5; i++ {

        fmt.Print(<- ch)

    }

}开了5个goroutine 然后又依次取数据 其实整个的执行过程细分的话 5个线的数据

依次流过信道ch main打印之
而宏观上即无缓冲信道的数据是 先到先出
但是 无缓冲信道并不存储数据 只负责数据的流通


缓冲信道 buffered channel

缓冲信道 不仅可以流通数据 还可以缓存数据 是有容量的 存入一个数据的话  可以先放在信道里 不必阻塞当前线而等待该数据取走

当缓冲信道达到满的状态的时候 就会表现出阻塞了 因为这时再也不能承载更多的数据了 「必须把 数据拿走 才可以流入数据」

在声明一个信道的时候 给make以第二个参数来指明它的容量(默认为0 即无缓冲)

var ch chan int = make(chan int, 2)// 写入2个元素都不会阻塞当前goroutine 存储个数达到2的时候会阻塞

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

func main() {

    ch := make(chan int, 3)

    ch <- 1

    ch <- 2

    ch <- 3

}

如果 再试图流入一个数据的话 信道ch会阻塞main线 报死锁

也就是说 缓冲信道会在满容量的时候加锁

缓冲信道是先进先出 可把缓冲信道看作为一个线程安全的队列

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

}


信道数据读取和信道关闭

一个一个读取信道太费事
Go语言允许 用range来读取信道

func main() {

    ch := make(chan int,3)

    ch <- 1

    ch <- 2

    ch <- 3

    for v := range ch {

        fmt.Println(v)

    }

}//fatal error: all goroutines are asleep - deadlock!

报死锁错误的 原因是 range 不等到信道关闭 是不会结束读取的 也就是如果 缓冲信道干涸了 那么range就会阻塞当前 goroutine 所以死锁咯

试着避免这种情况 比较容易想到的是读到信道为空的时候就结束读取

func main() {

ch := make(chan int,3)

ch <- 1

ch <- 2

ch <- 3

for v := range ch {

    fmt.Print(v,",")

    if len(ch) <= 0 { // 如果现有数据量为0 跳出循环

        break

    }

}

}//1,2,3,

注意检查信道大小的方法
不能在信道 存取 都在发生的时候 用于取出所有数据
因为只在ch中存了数据 现在一个一个往外取 信道大小是递减的

另一个方式是显式地关闭信道

ch := make(chan int,3)

ch <- 1

ch <- 2

ch <- 3

close(ch)// 显式地关闭信道

for v := range ch {

    fmt.Print(v,",")

}//1,2,3,

被关闭的信道会禁止数据流入 只读的
仍可从关闭的信道中取出数据 但是不能 写入数据了


等待多gorountine

用信道堵塞主线 等待开出去的所有 goroutine 跑完

开出很多小goroutine  各自跑各自的 最后跑完了向主线报告

如下2个版本的方案

只使用单个无缓冲信道阻塞主线 使用容量为goroutines数量的缓冲信道


var quit chan int // 只开一个信道

func foo(id int) {

    fmt.Print (id,",")

    quit <- 0 // ok finished

}

func main() {

    count := 10

    quit = make(chan int) // 无缓冲  // quit = make(chan int, count) // // 容量10

    for i := 0; i < count; i++ {

        go foo(i)

    }

    for i := 0; i < count; i++ {

        <- quit

    }

}//0,1,2,3,4,5,6,7,8,9,

对于方案2

把信道换成缓冲10的

quit = make(chan int, count) // // 容量10

区别仅仅在于一个是缓冲的 一个是非缓冲的

对于这个场景而言 两者都能完成任务 都是可以的

无缓冲的信道是一批数据一个一个的「流进流出」

缓冲信道则是一个一个存储 然后一起流出去