我怎样才能避免死锁

How can I avoid deadlock

看下面的代码片段。

package main

import (
    "errors"
    "fmt"
    "math/rand"
    "runtime"
    "sync"
    "time"
)

func random(min, max int) int {
    rand.Seed(time.Now().Unix())
    return rand.Intn(max-min) + min
}

func err1(rand int, chErr chan error, wg *sync.WaitGroup) {
    if rand == 1 {
        chErr <- errors.New("Error 1")
    }

    wg.Done()

}

func err2(rand int, chErr chan error, wg *sync.WaitGroup) {
    if rand == 2 {
        chErr <- errors.New("Error 2")
    }
    wg.Done()
}

func err3(rand int, chErr chan error, wg *sync.WaitGroup) {
    if rand == 3 {
        chErr <- errors.New("Error 3")
    }
    wg.Done()
}

func err4(rand int, chErr chan error, wg *sync.WaitGroup) {
    if rand == 3 {
        chErr <- errors.New("Error 4")
    }
    wg.Done()
}

func err5(rand int, chErr chan error, wg *sync.WaitGroup) {
    if rand == 4 {
        chErr <- errors.New("Error 5")
    }
    wg.Done()
}

func main() {

    runtime.GOMAXPROCS(runtime.NumCPU())

    chErr := make(chan error, 1)
    wg := new(sync.WaitGroup)

    //n := random(1, 8)
    n := 3
    fmt.Println(n)

    wg.Add(5)
    go err1(n, chErr, wg)
    go err2(n, chErr, wg)
    go err3(n, chErr, wg)
    go err4(n, chErr, wg)
    go err5(n, chErr, wg)

    fmt.Println("Wait")
    wg.Wait()
    select {
    case err := <-chErr:
        fmt.Println(err)
        close(chErr)
    default:
        fmt.Println("NO error, job done")
    }
}

这里如何避免死锁?我可以分配缓冲区长度 2,但也许它有更优雅的方法来解决问题。

我有意识地对函数 err3 和 err4 执行了 rand == 3。

既然你写的是故意在err3()err4()中使用rand == 3,那么可以有2个解决方案:

1。增加通道的缓冲区大小

chErr 通道的缓冲区大小增加到至少 2,因为在您的程序中使用 n = 3 可能会导致 2 个 goroutine 在通道上发送一个值。

2。使用非阻塞发送

最好在所有 errX() 函数中使用非阻塞通道发送(但至少在 err3()err4() 中,因为它们在相同条件下发送)和 select:

select {
case chErr <- errors.New("Error 3"):
default:
}

这将尝试在通道上发送一个 error 但如果它没有准备好(如果它已满,因为另一个 goroutine 已经发送了一个值),将选择 default 情况什么都不做。

Go Playground 上试用。

注意:这将 "lose" 错误之一,因为通道只能容纳一个错误,但无论如何您只能从中读取(接收)一个值。

您可以在 Go Concurrency Patterns: Timing out, moving on 博客文章中阅读有关非阻塞发送的更多信息。

您的程序已死锁,因为您的频道已满。

您的频道大小为一。然后你调用 wg.Wait() .. 等待调用 5 个函数。现在,一旦您到达 err3 .. rand == 3,因此您的频道会传递一个错误。

此时,您的频道已满,您只勾选了等待组中的 3 个项目。

err4 以值 3 .. 调用,这也想在您的频道上放一个错误。此时,它会阻塞 - 因为您的频道已满,没有任何内容从中弹出。

所以你的主 goroutine 会阻塞,因为你的等待组永远不会完成。

解决方法确实是让您的频道缓冲区更大。这样,当错误试图放置在通道上时 - 它不会阻塞,并且您的等待组有机会勾选其所有项目。

一般来说,不要陷入认为更大的缓冲区可以解决死锁的陷阱。这种方法可能适用于某些特定情况,但通常并非如此。

最好通过了解 goroutines 如何相互依赖来解决死锁。本质上,您必须消除相互依赖的通信循环。非阻塞发送想法(参见@izca 的回答)是一个有用的技巧,但不是唯一的。

有大量关于如何避免 deadlock/livelock 的知识。其中大部分来自奥卡姆在 80 年代和 90 年代流行的日子。 Jeremy Martin(无死锁设计策略 Concurrent Systems)、Peter Welch (Higher Level Paradigms) 和其他人。

  1. 客户端-服务器策略很简单:描述你的 Go-routine 网络作为一组通信服务器及其客户端;确保 网络图中没有循环 => 死锁是 淘汰了。

  2. I/o-par 是一种形成 Go 例程的环和圆环的方法,这样 结构内不会出现死锁;这是一个 允许循环但表现在 一般无死锁方式。

所以,我的策略是先减少 缓冲区大小,想想发生了什么,修复死锁。然后,根据基准重新引入缓冲区以提高性能。死锁是由通信图中的循环引起的。打破循环。

Related answer