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、信道使用时要注意是否构成死锁以及各种死锁产生的原因

如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!如果您想持续关注我的文章,请扫描二维码,关注JackieZheng的微信公众号,我会将我的文章推送给您,并和您一起分享我日常阅读过的优质文章。

原文地址:https://www.cnblogs.com/bigdataZJ/p/go-channel-deadlock.html

时间: 2024-10-10 10:22:56

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

C++语言学习(九)——多态

C++语言学习(九)--多态 C++中所谓的多态(polymorphism)是指由继承而产生的相关的不同的类,其对象对同一消息会作出不同的响应.    多态性是面向对象程序设计的一个重要特征,能增加程序的灵活性.可以减轻系统升级,维护,调试的工作量和复杂度. 多态是一种不同层次分类下的重要联系,是一种跨层操作. 一.多态实现的前提 赋值兼容规则是指在需要基类对象的任何地方都可以使用公有派生类的对象来替代.赋值兼容是一种默认行为,不需要任何的显式的转化步骤,只能发生在public继承方式中,是多态

go语言学习笔记

go语言学习笔记 go语言学习笔记(初级) 最近一直在学习go语言,因此打算学习的时候能够记录 一下笔记.我这个人之前是从来没有记录笔记的习惯, 一直以来都是靠强大的记忆力去把一些要点记住. 读书的时候因为一直都是有一个很安静和很专心的环境, 因此很多事情都能记得很清楚,思考的很透彻.但是随着 年纪不断增加,也算是经历了很多的事情,加上工作有时会让人 特别烦闷,很难把心好好静下来去学习,去思考大自然的终极 奥秘,因此需要记录一些东西,这些东西一方面可以作为一种自我激励 的机制,另一方面,也算是自

C++语言学习(十五)——C++抽象类与接口

C++语言学习(十五)--C++抽象类与接口 一.抽象类与接口 1.抽象类简介 面向对象的抽象类用于表示现实世界的抽象概念,是一种只能定义类型,不能产生对象的类(不能实例化),只能被继承并被重写相关函数,直接特征是相关函数没有完整实现.C++语言没有抽象类的概念,通过纯虚函数实现抽象类.纯虚函数是指定义原型的成员函数,C++中类如果存在纯虚函数就成为了抽象类.抽象类只能用作父类被继承,子类必须实现父类纯虚函数的具体功能,如果子类没实现纯虚函数,子类也为抽象类.抽象类不可以定义对象,但是可以定义指

go语言学习(五)——面向对象编程

主要讲的是"类"和接口&和其他传统语言不一样的地方挺多的,断断续续看了好几天 下面是我的练习代码 // GoStudy0219 project main.go /* go语言学习--面向对象编程(1) go中类型的值语义和引用语义 结构体(类)的定义和初始化 */ package main import ( "fmt" ) func main() { //几种"类"的初始化 v1 := &character{"Tom&q

关于c语言学习 谭浩强的书

2007-11-16 13:22:58|  分类: PROGRAMME |  标签: |举报 |字号大中小 订阅 广大有志于从事IT行业的同志们,在你们进入这一行之前千万请看这篇文章!太经典了!对你绝对有启发! 千万别买谭浩强和等级考试的书!!!!!! 整理别人的言论,请大家踊跃讨论!!!!!!!!!!!! 1:书皮上面有那么多的牛人题词,估计也许是自己的水平太低. 2:ANSI只给了两种方式:int main(void) {/*...*/}和 int main(int argc, char *

【转】朱兆祺教你如何攻破C语言学习、笔试与机试的难点(连载)

原文网址:http://bbs.elecfans.com/jishu_354666_1_1.html 再过1个月又是一年应届毕业生应聘的高峰期了,为了方便应届毕业生应聘,笔者将大学四年C语言知识及去年本人C语言笔试难点进行梳理,希望能对今年应届毕业生的应聘有所帮助. 2013年10月18日更新-->    攻破C语言这个帖子更新到这里,我不仅仅是为了补充大学学生遗漏的知识,我更重要的是希望通过我的经验,你们实际项目中的C语言写得漂亮,写出属于你的风格.“朱兆祺STM32手记”(http://bb

黑马程序员——c语言学习心得——函数传递二维数组

黑马程序员——c语言学习心得——函数传递二维数组 -------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- 一.定义指针的时候一定要初始化.   变量定义的时候给变量初始化,这是保证不出错的一个很好的习惯.尤其是在指针的使用上,如果我们没有给指针初始化,就会出现野指针,该指针的指向并不是我们所希望的,一旦错误的释放了这个指针,就会发生内存的访问.那么如何初始化指针变量呢,一般有以下几种方法:   1.初始化空指针   int* pInteger=N

C语言学习--八皇后问题

问题描述: 在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行.同一列或同一斜线上,问有多少种摆法. 程序设计: 1.一维数组a[17],数组分成三段,第一段a[0]用来标记八皇后安置完成:第二段a[1,8]用来标记列位置有无子,方便判断列冲突:第三段a[9,16]用来标记存储位置. 2.关键算法 递归判断位置,eightQueens . 3.对角线位置互斥判断, isDiagonal. 4.输出函数, printResult. 算法描述: 1.首次传入为数组a

R语言学习笔记

參考:W.N. Venables, D.M. Smith and the R DCT: Introduction to R -- Notes on R: A Programming Environment for Data Analysis and Graphics,2003. http://bayes.math.montana.edu/Rweb/Rnotes/R.html 前言:关于R 在R的官方教程里是这么给R下注解的:一个数据分析和图形显示的程序设计环境(A system for data