• channel 是怎么走上死锁这条路的



    本篇文章接着 hello world 的并发实现一文介绍 Go 的 channel 类型,同时进一步介绍 channel 的几种死锁情况,这些都是代码中很容易遇到的,要重点摘出来讲,防止一不留神程序就“死”了。

    1. 为什么需要 channel?

    channel 是一种通道类型,它通过发送和接收需要共享的资源,实现共享资源在 goroutine 之间的同步。说起来有点枯燥,来看一段代码:

    func speak(words *string) {
    	*words = "hello world"
    }
    
    func listen(words string) {
    	fmt.Printf("listen %s
    ", words)
    }
    
    func main() {
    	runtime.GOMAXPROCS(2)
    
    	var words string
    	go speak(&words)
    	go listen(words)
    }
    

    这段代码的初衷是开启两个 goroutine,其中 speak goroutine 写字符 "hello world" 给 words,而 listen goroutine 打印字符。执行代码发现什么都没打印,这是因为主main goroutine 还没等两个 goroutine 运行就提前结束了。那么加上 sync 等待每个 goroutine 运行,继续改写代码:

    func speak(words *string) {
    	defer wg.Done()
    	*words = "hello world"
    }
    
    func listen(words string) {
    	defer wg.Done()
    	fmt.Printf("listen %s
    ", words)
    }
    
    func main() {
    	runtime.GOMAXPROCS(2)
    
    	wg.Add(2)
    	var words string
    	go speak(&words)
    	go listen(words)
    	wg.Wait()
    }
    

    执行结果:

    listen
    
    

    可以看到两个 goroutine 都执行了,不过由于是并发执行,并没有实现我们想要的打印 hello world 效果。

    因此一个问题就冒出来了,各个 goroutine 间怎么知道彼此是否执行完毕了呢?或者换个问法,各个 goroutine 间怎么实现相互同步呢?
    这时候令人欢呼雀跃,让人疯狂的 channel 迈着矫健的步伐闪亮登场了,它彷佛在说快用我,快用我。没错,使用 channel 可以很好的解决这个问题。改下代码如下:

    func speak(words chan string) {
    	defer wg.Done()
    	words <- "hello world"
    }
    
    func listen(words chan string) {
    	defer wg.Done()
    	value := <-words
    	fmt.Printf("listen %s", value)
    }
    
    func main() {
    	runtime.GOMAXPROCS(2)
    
    	wg.Add(2)
    	words := make(chan string)
    	go speak(words)
    	go listen(words)
    	wg.Wait()
    }
    

    执行结果:

    listen hello world
    

    可以看到,引入 channel 很容易的打印出想要的效果。
    值得注意的是: 引入 channel,这里的 sync 还是必须的,如果不要的话 main goroutine 还是会不等两个 goroutine 运行完而提前走人。

    关于 channel 的使用,分类,关闭和遍历等内容不在本篇文章的讨论范围之内。接下来直接看使用 channel 会遇到的几种死锁情况。

    2. channel 死锁

    2.1 死锁名场面1

    代码示例:

    func speak(words chan string) {jing
    	defer wg.Done()
    	words <- "hello world"
    }
    
    func main() {
    	runtime.GOMAXPROCS(2)
    
    	wg.Add(2)
    	words := make(chan string)
    	go speak(words)
    	wg.Wait()
    }
    

    上述代码往 channel 中写入字符串,但是没有 goroutine 接收 channel 中的字符串导致程序阻塞在传值,引发死锁。

    func listen(words chan string) {
    	defer wg.Done()
    	value := <-words
    	fmt.Printf("listen %s", value)
    }
    
    func main() {
    	runtime.GOMAXPROCS(2)
    
    	wg.Add(2)
    	words := make(chan string)
    	go listen(words)
    	wg.Wait()
    }
    

    与前面死锁情况类似,这里没有 goroutine 往 channel 内写字符,导致程序阻塞在取值,引发死锁。

    2.2 死锁名场面2

    func speak(words chan string) {
    	defer wg.Done()
    	words <- "hello world"
    }
    
    func listen(words chan string) {
    	defer wg.Done()
    
    	disturber := make(chan string)
    	disturber <- "hi"
    
    	value := <-words
    	fmt.Printf("listen %s", value)
    }
    
    func main() {
    	runtime.GOMAXPROCS(2)
    
    	wg.Add(2)
    	words := make(chan string)
    	go speak(words)
    	go listen(words)
    	wg.Wait()
    }
    

    进一步的,这里添加了另一个 channel disturber,它将接收字符串。
    程序依然会出现死锁。因为两个 goroutine 都在等着通道 words 和 disturber 的值被接收,即便 value 会取 words 的值,但由于 disturber 的值没有接收,程序会一直阻塞在 disturber 传值这里,导致死锁。

    2.3 死锁名场面3

    改写代码如下:

    func cal_hello_num_(word string, ch chan int) {
    	defer wg.Done()
    
    	/* 通道是收方双方的,如果收和方是一对多的关系则需对方发进行限制,防止竞争抢占 */
    	mutex.Lock()
    	number++
    	ch <- number
    	fmt.Printf("say: %s, %d
    ", word, number)
    	mutex.Unlock()
    }
    
    func show_hello_num_(ch chan int) {
    	defer wg.Done()
    	for {
    		num, ok := <-ch
    		if !ok {
    			fmt.Printf("
    num: %d", num)
    			break
    		}
    	}
    }
    
    func num_say_hello_channel(ch chan int) {
    	go cal_hello_num_("w", ch)
    	go cal_hello_num_("o", ch)
    	go cal_hello_num_("r", ch)
    	go cal_hello_num_("l", ch)
    	go cal_hello_num_("d", ch)
    	go show_hello_num_(ch)
    }
    
    func main() {
    	runtime.GOMAXPROCS(2)
    	wg.Add(6)
    	ch := make(chan int)
    	num_say_hello_channel(ch)
    	wg.Wait()
    }
    

    代码很简单这里就不介绍了,查看执行结果:

    say: w, 1
    say: r, 2
    say: o, 3
    say: l, 4
    say: d, 5
    fatal error: all goroutines are asleep - deadlock!
    

    报死锁了,为什么呢?
    细看之下发现问题出在 for 循环这里,for 循环持续从通道 ch 读数据,当通道中无数据可读的时候相当于阻塞在通道取值,从而引发死锁。这是对于无缓冲通道的取值而言,对于有缓冲的通道也是适用的,有缓冲的通道数据读完了也相当于无缓冲的通道。

    知道了哪里错了在解决起来就不难了,在最后一个 goroutine 传通道值后将通道 close 掉,后面使用 for 读取已经关闭的通道将输出 ok 为 false:

    func cal_hello_num_(word string, ch chan int) {
    	defer wg.Done()
    
    	/* 经典案例: 通道是收方双方的,如果收和方是一对多的关系则需对方发进行限制,防止竞争抢占 */
    	mutex.Lock()
    	number++
    	ch <- number
    	fmt.Printf("say: %s, %d
    ", word, number)
    
    	if number == 5 {
    		fmt.Printf("close channel")
    		close(ch)
    	}
    
    	mutex.Unlock()
    }
    

    执行结果:

    say: w, 1
    say: o, 2
    say: r, 3
    say: l, 4
    say: d, 5
    close channel
    num: 0
    
    芝兰生于空谷,不以无人而不芳。
  • 相关阅读:
    初识EntityFramework6
    EntityFramework6 快速入门教程
    使用EntityFramework6连接MySQL
    Less的安装与配置
    Gitlab-CI持续集成之Runner配置和CI脚本
    CPU简单科普
    Mysql技能之【性能优化方案】
    自动化测试探索学习之路(1)
    性能测试知识之基础理论
    http协议、cookie及session
  • 原文地址:https://www.cnblogs.com/xingzheanan/p/14665282.html
Copyright © 2020-2023  润新知