
本文针对Go语言并发编程中常见的死锁问题,以观察者模式的实现为例,深入剖析了死锁产生的原因,并提供了两种有效的解决方案:使用带缓冲的channel以及利用sync.WaitGroup进行goroutine同步。通过本文的学习,开发者可以更好地理解Go语言的并发机制,避免死锁,编写出更健壮的并发程序。
在Go语言中,并发编程是其强大功能之一,但同时也带来了诸如死锁等问题。本文将通过一个基于观察者模式的示例,深入探讨Go语言中死锁的产生原因以及相应的解决方案。
以下代码尝试实现一个简单的观察者模式,其中Publisher负责发布消息,Subscriber负责监听消息。
package main
import (
"fmt"
)
type Publisher struct {
listeners []chan int
}
type Subscriber struct {
Channel chan int
Name string
}
func (p *Publisher) Sub(c chan int) {
p.listeners = append(p.listeners, c)
}
func (p *Publisher) Pub(m int, quit chan int) {
for _, c := range p.listeners {
c <- m
}
quit <- 0
}
func (s *Subscriber) ListenOnChannel() {
data := <-s.Channel
fmt.Printf("Name: %v; Data: %v\n", s.Name, data)
}
func main() {
quit := make(chan int)
p := &Publisher{}
subscribers := []*Subscriber{
{Channel: make(chan int), Name: "1"},
{Channel: make(chan int), Name: "2"},
{Channel: make(chan int), Name: "3"},
}
for _, v := range subscribers {
p.Sub(v.Channel)
go v.ListenOnChannel()
}
p.Pub(2, quit)
<-quit
}这段代码在运行时会产生死锁错误:"fatal error: all goroutines are asleep - deadlock!"。
死锁的根本原因在于quit channel的使用方式。quit channel的缓冲区大小为0,这意味着发送和接收操作必须同时进行。在Pub方法中,首先向所有listener的channel发送消息,然后尝试向quit channel发送消息。然而,main函数中的<-quit操作会阻塞,直到Pub方法向quit channel发送消息。
问题在于,Pub方法在同一个goroutine中发送和接收quit channel,导致互相等待,从而形成死锁。
一种解决方案是在每个Subscriber的goroutine中发送quit信号,并在main函数中接收所有quit信号。
package main
import (
"fmt"
)
type Publisher struct {
listeners []chan int
}
type Subscriber struct {
Channel chan int
Name string
}
func (p *Publisher) Sub(c chan int) {
p.listeners = append(p.listeners, c)
}
func (p *Publisher) Pub(m int) {
for _, c := range p.listeners {
c <- m
}
}
func (s *Subscriber) ListenOnChannel(quit chan int) {
data := <-s.Channel
fmt.Printf("Name: %v; Data: %v\n", s.Name, data)
quit <- 0 // Subscriber发送quit信号
}
func main() {
quit := make(chan int)
p := &Publisher{}
subscribers := []*Subscriber{
{Channel: make(chan int), Name: "1"},
{Channel: make(chan int), Name: "2"},
{Channel: make(chan int), Name: "3"},
}
for _, v := range subscribers {
p.Sub(v.Channel)
go v.ListenOnChannel(quit) // 传递quit channel
}
p.Pub(2)
// 接收所有Subscriber的quit信号
for i := 0; i < len(subscribers); i++ {
<-quit
}
}在这个修改后的版本中,ListenOnChannel方法接收quit channel,并在完成打印后向其发送信号。main函数循环接收所有Subscriber的quit信号,确保所有goroutine都已完成。
另一种更优雅的解决方案是使用sync.WaitGroup来同步goroutine。
package main
import (
"fmt"
"sync"
)
type Publisher struct {
listeners []chan int
}
type Subscriber struct {
Channel chan int
Name string
}
func (p *Publisher) Sub(c chan int) {
p.listeners = append(p.listeners, c)
}
func (p *Publisher) Pub(m int) {
for _, c := range p.listeners {
c <- m
}
}
func (s *Subscriber) ListenOnChannel(wg *sync.WaitGroup) {
defer wg.Done() // goroutine完成时调用Done
data := <-s.Channel
fmt.Printf("Name: %v; Data: %v\n", s.Name, data)
}
func main() {
var wg sync.WaitGroup
p := &Publisher{}
subscribers := []*Subscriber{
{Channel: make(chan int), Name: "1"},
{Channel: make(chan int), Name: "2"},
{Channel: make(chan int), Name: "3"},
}
for _, v := range subscribers {
p.Sub(v.Channel)
wg.Add(1) // 增加WaitGroup计数器
go v.ListenOnChannel(&wg) // 传递WaitGroup
}
p.Pub(2)
wg.Wait() // 等待所有goroutine完成
}在这个版本中,sync.WaitGroup用于等待所有Subscriber的goroutine完成。wg.Add(1)在每个goroutine启动前增加计数器,defer wg.Done()在goroutine完成时减少计数器,wg.Wait()会阻塞直到计数器变为0。
通过以上示例和分析,我们了解了Go语言中死锁的产生原因以及两种有效的解决方案。在实际开发中,应该根据具体情况选择合适的解决方案,并时刻注意避免潜在的并发问题。
以上就是Go并发编程中的死锁问题及解决方案:基于观察者模式的实践的详细内容,更多请关注php中文网其它相关文章!
编程怎么学习?编程怎么入门?编程在哪学?编程怎么学才快?不用担心,这里为大家提供了编程速学教程(入门课程),有需要的小伙伴保存下载就能学习啦!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号