0
点赞
收藏
分享

微信扫一扫

TCP的粘包、拆包、解决方案以及Go语言实现

SDKB英文 2023-07-04 阅读 91
golang死锁

go语言虽然号称协程之间必须使用channel通信,但是如果使用不当,非常容易形成deadlock死锁。下面的代码就是这样的一个例子

package main

import "fmt"

func doWork(id int, dataChan chan int, done chan bool) {
	for data := range dataChan { 
		fmt.Printf("Worker %d received %c\n", id, data)		
		done <- true
	}
}

func createChannels(id int) Worker {
	ch := Worker{
		data: make(chan int),
		done: make(chan bool),
	}
	go doWork(id, ch.data, ch.done)
	return ch
}

func chanRun() {
	var channels [10]Worker
	for i := 0; i < 10; i++ {
		channels[i] = createChannels(i)
	}

	for i, ch := range channels {
		ch.data <- 'a' + i
		//<-ch.done
	}

	for i, ch := range channels {
		ch.data <- 'A' + i
		//<-ch.done
	}

	for _, ch := range channels {
		<-ch.done
		<-ch.done
	}

}

type Worker struct {
	data chan int
	done chan bool
}

func main() {
	chanRun()
}

为什么会形成死锁? 这里需要对无缓冲channel有深刻的理解。

无缓冲channel需要发送方和接收方同时在线,只要有一方没有就位,就会引发死锁!

在在doWork这段代码里,dataChan和done这两个通道是耦合在一起了,因为他们在一个同步代码块里,所以会出现互相等待的问题。  一旦其中一个通道被阻塞,另外一个通道也会因为等待陷入无法就绪状态,从而产生deadlock死锁问题。

这里的解决死锁的关键就是解耦,也就是将两个channel的因果关系破坏掉,避免互相等待。

具体来说:可以给其中的一个channel操作再开一个协程,也就是并发执行,这样就脱离了同步代码块,在这个例子里面,如下修改即可。

func doWork(id int, dataChan chan int, done chan bool) {
	for data := range dataChan { // 注意range是阻塞式读取channel,所以后面的done <- true如果不开协程也会被阻塞住
		fmt.Printf("Worker %d received %c\n", id, data)
		//给done这个channel单独再可一个协程,并发执行,脱离doWork主程序,
        // 从而避免dataChan和done这两个channel在同步代码块中的互相等待    
		go func() { done <- true }()
		//done <- true
	}
}

还有一个解决办法: 将两个通道中任意一个通道变成有缓冲通道

此时done通道也就无需单独开协程。

因此我们使用无缓冲channel时一定要非常小心,为避免死锁, 总体有以下两大原则:

1. 尽量避免在同步代码中使用两个channel的情况,避免channel之间出现耦合、嵌套的情况,因为容易出现两个channel互相等待的问题; 如果确实没法避免,则必须给其中一个channel单独再开一个协程,类似于下面的代码:

2. 往无缓冲channel发送数据时务必单独再开一个协程

如果没有把握处理好无缓冲channle,为了安全起见,建议使用有缓冲channel

顺附:有缓冲channel和无缓冲channel的区别说明

有缓冲channel和无缓冲channel本质上就是一个同步和异步的区别。

无缓冲:就是完全同步,发送和接收方紧紧耦合在一起。

有缓冲: 异步工作。 通道未满之前,发送方和接收方各自独立工作:也就是都不会阻塞。这个有点类似于消息队列MQ.此时通道起到了解耦的作用。通道满了以后,就是同步通信,此时发送方阻塞。

举报

相关推荐

0 条评论