在监视器 goroutine 中递归发送

Recursive sends in monitor goroutine

在我编写的一个简单的计时器调度程序中,我正在使用监视器 goroutine 来同步 start/stop 和计时器完成事件。

监视器 goroutine,当被剥离到最基本的部分时,看起来像这样:

actions := make(chan func(), 1024)
// monitor goroutine
go func() {
    for a := range actions {
        a()
    }
}()
actions <- func() {
    actions <- func() {
        // causes deadlock when buffer size is reached
    }
}

这很好用,直到发送一个动作再发送另一个动作。 计划的操作可能会安排另一个操作,这会在达到缓冲区大小时导致死锁。

有没有什么干净的方法可以解决这个问题而不求助于共享状态(我已经在我的特定问题中尝试过,但是非常丑陋)?

这个问题是因为当你的monitor goroutine "takes out"(接收)一个值(一个函数)并执行它(这也发生在monitor goroutine上),在执行期间它发送一个值在监控频道上。

这本身不会导致死锁,因为当函数执行时(a()),它已经从缓冲通道中取出,所以它至少有一个空闲space ,因此在 a() 内的通道上发送新值可以在不阻塞的情况下继续进行。

如果有其他 goroutine 也可能在受监控的通道上发送值,这就是您的情况,则可能会出现问题。

避免死锁的一种方法是,如果正在执行的函数试图 "put back"(发送)一个不在同一个 goroutine(即监视器 goroutine)中的函数,而是在一个新的 goroutine 中,所以监视器 goroutine 没有被阻塞:

actions <- func() {
    // Send new func value in a new goroutine:
    // this will never block the monitor goroutine
    go func() {
        actions <- func() {
            // No deadlock.
        }
    }()
}

通过这样做,即使 actions 的缓冲区已满,monitor goroutine 也不会被阻塞,因为在它上面发送一个值将发生在一个新的 goroutine 中(它可能会被阻塞直到有空闲 space 在缓冲区中)。

如果你想避免在 actions 上发送值时总是生成一个新的 goroutine,你可以使用 select 首先尝试发送它而不生成一个新的 goroutine。只有当 actions 的缓冲区已满并且无法不阻塞地发送时,您是否应该生成一个 goroutine 用于发送以避免死锁,这 - 根据您的实际情况可能很少发生,并且在这种情况下无论如何都不可避免地要避免死锁:

actions <- func() {
    newfv := func() { /* do something */ }
    // First try to send with select:
    select {
    case actions <- newfv:
        // Success!
    default:
        // Buffer is full, must do it in new goroutine:
        go func() {
            actions <- newfv
        }()
    }
}

如果你需要在很多地方这样做,建议为它创建一个辅助函数:

func safeSend(fv func()) {
    // First try to send with select:
    select {
    case actions <- fv:
        // Success!
    default:
        // Buffer is full, must do it in new goroutine:
        go func() {
            actions <- fv
        }()
    }
}

并使用它:

actions <- func() {
    safeSend(func() {
        // something to do
    })
}