在监视器 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
})
}
在我编写的一个简单的计时器调度程序中,我正在使用监视器 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
})
}