英文:
Recursive sends in monitor goroutine
问题
在我正在编写的一个简单定时器调度器中,我正在使用一个监视器 goroutine 来同步启动/停止和定时器完成事件。
当将监视器 goroutine 简化为基本形式时,它看起来像这样:
actions := make(chan func(), 1024)
// 监视器 goroutine
go func() {
for a := range actions {
a()
}
}()
actions <- func() {
actions <- func() {
// 当缓冲区大小达到时会导致死锁
}
}
这个方法很好用,直到发送一个发送另一个动作的动作为止。
一个预定的动作可能会安排另一个动作,当缓冲区大小达到时会导致死锁。
有没有一种干净的方法来解决这个问题,而不需要使用共享状态(我在我的特定问题中尝试过,但非常丑陋)?
英文:
In a simple scheduler for timers I'm writing, I'm making use of a monitor goroutine to sync start/stop and timer done events.
The monitor goroutine, when stripped down to the essential, looks like this:
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
}
}
This works great, until an action is sent that sends another action.
It's possible for a scheduled action to schedule another action, which causes a deadlock when the buffer size is reached.
Is there any clean way to solve this issue without resorting to shared state (which I've tried in my particular problem, but is quite ugly)?
答案1
得分: 0
问题出在于当你的监视器 goroutine "取出"(接收)一个值(一个函数)并执行它时(这也发生在监视器 goroutine 上),在其执行过程中它在被监视的通道上发送一个值。
这本身不会导致死锁,因为当函数被执行(a()
)时,它已经从缓冲通道中取出,所以至少有一个空闲空间,因此在 a()
中发送新值到通道上可以继续进行而不会阻塞。
如果有其他的 goroutine 也可能在被监视的通道上发送值,那么问题可能会出现,这就是你的情况。
避免死锁的一种方法是,如果正在执行的函数试图在一个新的 goroutine 中而不是在同一个 goroutine(即监视器 goroutine)中“放回”(发送)一个函数,这样监视器 goroutine 就不会被阻塞:
actions <- func() {
// 在一个新的 goroutine 中发送新的函数值:
// 这不会阻塞监视器 goroutine
go func() {
actions <- func() {
// 没有死锁。
}
}()
}
通过这样做,即使 actions
的缓冲区已满,在其上发送一个值也不会阻塞监视器 goroutine,因为它将在一个新的 goroutine 中发送值(该 goroutine 可能会被阻塞,直到缓冲区有空闲空间)。
如果你想避免每次在 actions
上发送值时都生成一个新的 goroutine,你可以使用 select
来首先尝试发送而不生成新的 goroutine。只有当 actions
的缓冲区已满且无法发送而不阻塞时,才应该生成一个用于发送的 goroutine,以避免死锁。根据你的实际情况,这种情况可能很少发生,而且在这种情况下,为了避免死锁是不可避免的:
actions <- func() {
newfv := func() { /* 做一些事情 */ }
// 首先尝试使用 select 发送:
select {
case actions <- newfv:
// 成功!
default:
// 缓冲区已满,必须在新的 goroutine 中执行:
go func() {
actions <- newfv
}()
}
}
如果你需要在多个地方这样做,建议创建一个辅助函数:
func safeSend(fv func()) {
// 首先尝试使用 select 发送:
select {
case actions <- fv:
// 成功!
default:
// 缓冲区已满,必须在新的 goroutine 中执行:
go func() {
actions <- fv
}()
}
}
然后使用它:
actions <- func() {
safeSend(func() {
// 做一些事情
})
}
英文:
The problem arises from the fact that when your monitor goroutine "takes out" (receives) a value (a function) and executes it (this also happens on the monitor goroutine), during its execution it sends a value on the monitored channel.
This in itself would not cause deadlock as when the function is executed (a()
), it is already taken out of the buffered channel, so there is at least one free space in it, so sending a new value on the channel inside a()
could proceed without blocking.
Problem may arise if there are other goroutines which may also send values on the monitored channel, which is your case.
One way to avoid the deadlock is that if the function being executed tries to "put back" (send) a function not in the same goroutine (which is the monitor goroutine), but in a new goroutine, so the monitor goroutine is not blocked:
actions <- func() {
// Send new func value in a new goroutine:
// this will never block the monitor goroutine
go func() {
actions <- func() {
// No deadlock.
}
}()
}
By doing this, the monitor goroutine will not be blocked even if buffer of actions
is full, because sending a value on it will happen in a new goroutine (which may be blocked until there is free space in the buffer).
If you want to avoid always spawning a new goroutine when sending a value on actions
, you may use a select
to first try to send it without spawning a new goroutine. Only when buffer of actions
is full and cannot be sent without blocking, shall you spawn a goroutine for sending to avoid deadlock, which - depending on your actual case may happen very rarely, and in this case it's inevitable anyway to avoid deadlock:
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
}()
}
}
If you have to do this in many places, it's recommended to create a helper function for it:
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
}()
}
}
And using it:
actions <- func() {
safeSend(func() {
// something to do
})
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论