【问题标题】:How can I avoid deadlock我怎样才能避免死锁
【发布时间】:2015-01-22 18:10:38
【问题描述】:

看下面的代码sn-p。

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。

【问题讨论】:

  • 你真的打算在err3err4 中都有if rand == 3 吗?

标签: go


【解决方案1】:

由于您的频道已满,您的程序陷入僵局。

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

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

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

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

修复确实是使您的频道缓冲区更大。这样,当尝试将错误放在通道上时 - 它不会阻塞,并且您的等待组有机会勾选其所有项目。

【讨论】:

  • 这是根本错误的。添加缓冲在某些情况下可能会起作用,但这是一种冒险的策略。看我的回答。
  • @Rick-777 有了这样一个小例子,OP 清楚地放在一起,我不确定如何解释你在回答中提到的各种事情。所以,我选择直接回答这个问题。老实说,我发现很难将更大的模式应用于这些简单的示例——随意提供一个。我一定会支持你。
【解决方案2】:

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

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

关于如何避免死锁/活锁,有大量的知识。其中大部分来自 Occam 在 80 年代和 90 年代流行的日子。 Jeremy Martin (Design Strategy for Deadlock-Free Concurrent Systems)、Peter Welch (Higher Level Paradigms) 等人提供了一些特殊的宝石。

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

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

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

Related answer

【讨论】:

    【解决方案3】:

    由于您在err3()err4() 中都故意使用rand == 3,因此可以有两种解决方案:

    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 上试用。

    注意:这将“丢失”其中一个错误,因为通道只能保存一个错误,但无论如何您只能从中读取(接收)一个值。

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

    【讨论】:

    • 我有意识地对函数 err3 和 err4 做了rand == 3
    • @zero_coding 编辑了我的答案,包括 2 个可能的解决方案。
    猜你喜欢
    • 1970-01-01
    • 2013-07-24
    • 1970-01-01
    • 2013-10-08
    • 1970-01-01
    • 2022-12-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多