阻塞死锁详解

目录

一、通道阻塞/死锁示例

二、死锁解决办法

1、方法一:先消费channel

2、方法二:用缓冲通道

三、信道数据的进出顺序

1、无缓冲信道

2、缓冲信道

四、等待多gorountine的方案


一、通道阻塞/死锁示例

死锁:所有的线程或进程都在等待资源的释放

1)死锁示例一

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

//执行这个程序你会看到Go报这样的错误:
fatal error: all goroutines are asleep - deadlock!

如上的程序中, 只有一个goroutine, 所以当你向里面加数据或者存数据的话,都会锁死信道, 并且阻塞当前 goroutine

2)死锁示例二

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

3)死锁总结

默认的,信道的存消息和取消息都是阻塞的,也就是说,无缓冲的信道在取消息和存消息的时候都会挂起当前的goroutine,除非另一端已经准备好。

非缓冲信道上如果发生了流入无流出,或者流出无流入,也就导致了死锁。或者这样理解 Go启动的所有goroutine里的非缓冲信道一定要一个线里存数据,一个线里取数据,要成对才行 。所有不成对向的信道存取数据几乎都会发生死锁,但是也有特殊的,如下:

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

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

        程序正常退出了,很简单,并不是我们那个总结不起作用了,main又没等待其它goroutine,自己先跑完了, 所以没有数据流入c信道,一共执行了一个goroutine, 并且没有发生阻塞,所以没有死锁错误。所说可以让信道来阻塞主线( <- c)

二、死锁解决办法

1、方法一:先消费channel

package main

import (
    "fmt"
)

func f1(in chan int) {
    fmt.Println(<-in)
}

func main() {
    out := make(chan int)
    //调整下位置,先消费
    go f1(out)
    out <- 2
}

2、方法二:用缓冲通道

        加buffer变成有缓冲信道,缓冲信道它是有容量的,存入一个数据的话 , 可以先放在信道里,不必阻塞当前线而等待该数据取走。当缓冲信道达到满的状态的时候,就会表现出阻塞了,因为这时再也不能承载更多的数据了,「你们必须把 数据拿走,才可以流入数据」。

缓冲信道ch可以无缓冲的流入3个元素

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

        如果你再试图流入一个数据的话,信道ch会阻塞main线, 报死锁。也就是说,缓冲信道会在满容量的时候加锁。

三、信道数据的进出顺序

1、无缓冲信道

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)
    }
}

无缓冲信道的数据是先到先出,但是 无缓冲信道并不存储数据,只负责数据的流通

2、缓冲信道

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
}

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

四、等待多gorountine的方案

那好,我们回到最初的一个问题,使用信道堵塞主线,等待开出去的所有goroutine跑完。

这是一个模型,开出很多小goroutine, 它们各自跑各自的,最后跑完了向主线报告。

我们讨论如下2个版本的方案:

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

对于方案1, 示例的代码大概会是这个样子:

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

func foo(id int) {
    fmt.Println(id)
    quit <- 0 // ok, finished
}

func main() {
    count := 1000
    quit = make(chan int) // 无缓冲
    
    for i := 0; i < count; i++ {
        go foo(i)
    }

    for i := 0; i < count; i++ {
        <- quit
    }
}

对于方案2, 把信道换成缓冲1000的:

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

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

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

  • 无缓冲的信道是一批数据一个一个的「流进流出」
  • 缓冲信道则是一个一个存储,然后一起流出去

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值