Go语言学习——channel的死锁其实没那么复杂

1 为什么会有信道

协程(goroutine)算是Go的一大新特性,也正是这个大杀器让Go为很多路人驻足欣赏,让信徒们为之欢呼津津乐道。

协程的使用也很简单,在Go中使用关键字“go“后面跟上要执行的函数即表示新启动一个协程中执行功能代码。

func main() {
    go test()
    fmt.Println("it is the main goroutine")
    time.Sleep(time.Second * 1)
}

func test() {
    fmt.Println("it is a new goroutine")
}

可以简单理解为,Go中的协程就是一种更轻、支持更高并发的并发机制。

仔细看上面的main函数中有一个休眠一秒的操作,如果去掉该行,则打印结果中就没有“it is a new goroutine”。这是因为新启的协程还没来得及运行,主协程就结束了。

所以这里有个问题,我们怎么样才能让各个协程之间能够知道彼此是否执行完毕呢?

显然,我们可以通过上面的方式,让主协程休眠一秒钟,等等子协程,确保子协程能够执行完。但作为一个新型语言不应该使用这么low的方式啊。连Java这位老前辈都有Future这种异步机制,而且可以通过get方法来阻塞等待任务的执行,确保可以第一时间知晓异步进程的执行状态。

所以,Go必须要有过人之处,即另一个让路人侧目,让信徒为之疯狂的特性——信道(channel)。

2 信道如何使用

信道可以简单认为是协程goroutine之间一个通信的桥梁,可以在不同的协程里互通有无穿梭自如,且是线程安全的。

2.1 信道分类

信道分为两类

无缓冲信道

ch := make(chan string)

有缓冲信道

ch := make(chan string, 2)

2.2 两类信道的区别

1、从声明方式来看,有缓冲带了容量,即后面的数字,这里的2表示信道可以存放两个stirng类型的变量

2、无缓冲信道本身不存储信息,它只负责转手,有人传给它,它就必须要传给别人,如果只有进或者只有出的操作,都会造成阻塞。有缓冲的可以存储指定容量个变量,但是超过这个容量再取值也会阻塞。

2.3 两种信道使用举例

无缓冲信道

func main() {
    ch := make(chan string)
    go func() {
        ch <- "send"
    }()
    
    fmt.Println(<-ch)
}

在主协程中新启一个协程且是匿名函数,在子协程中向通道发送“send”,通过打印结果,我们知道在主线程使用<-ch接收到了传给ch的值。

<-ch是一种简写方式,也可以使用str := <-ch方式接收信道值。

上面是在子协程中向信道传值,并在主协程取值,也可以反过来,同样可以正常打印信道的值。

func main() {
    ch := make(chan string)
    go func() {
        fmt.Println(<-ch)
    }()

    ch <- "send"
}

有缓冲信道

func main() {
    ch := make(chan string, 2)
    ch <- "first"
    ch <- "second"
    
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

执行结果为

first
second

信道本身结构是一个先进先出的队列,所以这里输出的顺序如结果所示。

从代码来看这里也不需要重新启动一个goroutine,也不会发生死锁(后面会讲原因)。

3 信道的关闭和遍历

3.1 关闭

信道是可以关闭的。对于无缓冲和有缓冲信道关闭的语法都是一样的。

close(channelName)

注意信道关闭了,就不能往信道传值了,否则会报错。

func main() {
    ch := make(chan string, 2)
    ch <- "first"
    ch <- "second"

    close(ch)

    ch <- "third"
}

报错信息

panic: send on closed channel

3.2 遍历

有缓冲信道是有容量的,所以是可以遍历的,并且支持使用我们熟悉的range遍历。

func main() {
    chs := make(chan string, 2)
    chs <- "first"
    chs <- "second"

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

输出结果为

first
second
fatal error: all goroutines are asleep - deadlock!

没错,如果取完了信道存储的信息再去取信息,也会死锁(后面会讲)

4 信道死锁

有了前面的介绍,我们大概知道了信道是什么,如何使用信道。

下面就来说说信道死锁的场景和为什么会死锁(有些是自己的理解,可能有偏差,如有问题请指正)。

4.1 死锁现场1

func main() {
    ch := make(chan string)
    
    ch <- "channelValue"
}
func main() {
    ch := make(chan string)
    
    <-ch
}

这两种情况,即无论是向无缓冲信道传值还是取值,都会发生死锁。

原因分析

如上场景是在只有一个goroutine即主goroutine的,且使用的是无缓冲信道的情况下。

前面提过,无缓冲信道不存储值,无论是传值还是取值都会阻塞。这里只有一个主协程的情况下,第一段代码是阻塞在传值,第二段代码是阻塞在取值。因为一直卡住主协程,系统一直在等待,所以系统判断为死锁,最终报deadlock错误并结束程序。

延伸

func main() {
    ch := make(chan string)
    go func() {
        ch <- "send"
    }()
}

这种情况不会发生死锁。

有人说那是因为主协程发车太快,子协程还没看到,车就开走了,所以没来得及抱怨(deadlock)就结束了。

其实不是这样的,下面举个反例

func main() {
    ch := make(chan string)
    go func() {
        ch <- "send"
    }()

    time.Sleep(time.Second * 3)
}

这次主协程等你了三秒,三秒你总该完事了吧?!

但是从执行结果来看,并没有子协程因为一直阻塞就造成报死锁错误。

这是因为虽然子协程一直阻塞在传值语句,但这也只是子协程的事。外面的主协程还是该干嘛干嘛,等你三秒之后就发车走人了。因为主协程都结束了,所以子协程也只好结束(毕竟没搭上车只能回家了,光杵在哪也于事无补)

4.2 死锁现场2

紧接着上面死锁现场1的延伸场景,我们提到延伸场景没有死锁是因为主协程发车走了,所以子协程也只能回家。也就是两者没有耦合的关系。

如果两者通过信道建立了联系还会死锁吗?

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    go func() {
        ch2 <- "ch2 value"
        ch1 <- "ch1 value"
    }()
    
    <- ch1
}

执行结果为

fatal error: all goroutines are asleep - deadlock!

没错,这样就会发生死锁。

原因分析

上面的代码不能保证是主线程的<-ch1先执行还是子协程的代码先执行。

如果主协程先执行到<-ch1,显然会阻塞等待有其他协程往ch1传值。终于等到子协程运行了,结果子协程运行ch2 <- "ch2 value"就阻塞了,因为是无缓冲,所以必须有下家接收值才行,但是等了半天也没有人来传值。

所以这时候就出现了主协程等子协程的ch1,子协程在等ch2的接收者,ch1<-“ch1 value”语句迟迟拿不到执行权,于是大家都在相互等待,系统看不下去了,判定死锁,程序结束。

相反执行顺序也是一样。

延伸

有人会说那我改成这样能避免死锁吗

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    go func() {
        ch2 <- "ch2 value"
        ch1 <- "ch1 value"
    }()

    <- ch1
    <- ch2
}

不行,执行结果依然是死锁。因为这样的顺序还是改变不了主协程和子协程相互等待的情况,即死锁的触发条件。

改为下面这样就可以正常结束

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    go func() {
        ch2 <- "ch2 value"
        ch1 <- "ch1 value"
    }()

    <- ch2
    <- ch1
}

借此,通过下面的例子再验证上面死锁现场1是因为主协程没受到死锁的影响所以不会报死锁错误的问题

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    go func() {
        ch2 <- "ch2 value"
        ch1 <- "ch1 value"
    }()

    go func() {
        <- ch1
        <- ch2
    }()

    time.Sleep(time.Second * 2)
}

我们刚刚看到如果

<- ch1
<- ch2

放到主协程,则会因为相互等待发生死锁。但是这个例子里,将同样的代码放到一个新启的协程中,尽管两个子协程存在阻塞死锁的情况,但是不会影响主协程,所以程序执行不会报死锁错误。

4.3 死锁现场3

func main() {
    chs := make(chan string, 2)
    chs <- "first"
    chs <- "second"

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

输出结果为

first
second
fatal error: all goroutines are asleep - deadlock!

原因分析

为什么会在输出完chs信道所有缓存值后会死锁呢?

其实也很简单,虽然这里的chs是带有缓冲的信道,但是容量只有两个,当两个输出完之后,可以简单的将此时的信道等价于无缓冲的信道。

显然对于无缓冲的信道只是单纯的读取元素是会造成阻塞的,而且是在主协程,所以和死锁现场1等价,故而会死锁。

5 总结

1、信道是协程之间沟通的桥梁

2、信道分为无缓冲信道和有缓冲信道

3、信道使用时要注意是否构成死锁以及各种死锁产生的原因

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容

  • Go是并发语言,而不是并行语言。 一、并发和并行的区别 •并发(concurrency)是指一次处理大量事情的能力...
    学生黄哲阅读 2,292评论 1 17
  • 本节学习 什么是信道? 如何声明信道? 信道如何收发数据? 什么是死锁? 什么是单向信道? 如何关闭信道? 使用 ...
    酷走天涯阅读 535评论 0 0
  • Go语言并发 Go 是并发式语言,而不是并行式语言。 并发是指立即处理多个任务的能力。 Go 编程语言原生支持并发...
    kakarotto阅读 1,887评论 0 7
  • 缓冲信道 之前看到的都是无缓冲信道,无缓冲信道的发送和接收过程是阻塞的。我们还可以创建一个有缓冲(Buffer)的...
    kakarotto阅读 367评论 0 0
  • 多线程同时执行叫做并行 并发就是在不同线程中来回切换执行来达到并行的效果就是并发 通过go可以在当前线程中开启一个...
    AuglyXu阅读 6,785评论 0 9