面试题答案
一键面试可能导致死锁的常见情况
- 双向通信死锁:两个或多个goroutine互相等待对方通过channel发送数据,形成循环依赖。例如:
package main
import "fmt"
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
ch1 <- 1
fmt.Println(<-ch2)
}()
go func() {
ch2 <- 2
fmt.Println(<-ch1)
}()
select {}
}
在上述代码中,两个goroutine互相等待对方发送数据,导致死锁。
- 无缓冲channel读写不匹配:只在一个goroutine中对无缓冲channel进行写操作,而没有其他goroutine同时进行读操作;或者只进行读操作,而没有写操作。例如:
package main
import "fmt"
func main() {
ch := make(chan int)
ch <- 1
fmt.Println(<-ch)
}
这里在主goroutine中向无缓冲channel ch
写入数据,但没有其他goroutine来读取,导致死锁。
-
关闭channel不当:在多个goroutine共享的channel关闭时,如果处理不当,可能导致死锁。例如,一个goroutine关闭channel后,另一个goroutine还在尝试向已关闭的channel写数据。
-
资源竞争与死锁:当多个goroutine竞争共享资源,并且获取资源的顺序不一致时,可能导致死锁。比如多个goroutine同时获取多个互斥锁,获取顺序不同就可能造成死锁。
避免死锁的方法
- 合理设计通信逻辑:确保goroutine之间的通信不会形成循环依赖。例如,对于上述双向通信死锁的代码,可以修改为:
package main
import "fmt"
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
ch1 <- 1
fmt.Println(<-ch1)
}()
go func() {
data := <-ch1
ch1 <- data + 1
}()
select {}
}
这里通过调整通信逻辑,避免了互相等待的情况。
- 使用缓冲channel:在适当情况下,使用有缓冲的channel可以减少死锁风险。例如:
package main
import "fmt"
func main() {
ch := make(chan int, 1)
ch <- 1
fmt.Println(<-ch)
}
有缓冲的channel允许在没有读操作时先写入一定数量的数据,避免了无缓冲channel读写不匹配导致的死锁。
- 正确关闭channel:确保只有一个goroutine负责关闭channel,并且在关闭前检查是否所有需要的数据都已发送。例如:
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}()
for data := range ch {
fmt.Println(data)
}
}
这里通过for... range
循环来读取channel数据,直到channel关闭,避免了对已关闭channel的无效操作。
- 使用锁和资源管理:对于共享资源竞争问题,使用互斥锁(
sync.Mutex
)等机制来保护共享资源,并确保所有goroutine以一致的顺序获取锁。例如:
package main
import (
"fmt"
"sync"
)
var mu sync.Mutex
var data int
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
mu.Lock()
data++
mu.Unlock()
}()
go func() {
defer wg.Done()
mu.Lock()
fmt.Println(data)
mu.Unlock()
}()
wg.Wait()
}
通过互斥锁保证了对共享变量data
的安全访问,避免了因资源竞争导致的死锁。