监视器 goroutine 中的递归发送

huangapple go评论83阅读模式
英文:

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 &lt;- func() {
    actions &lt;- 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 &lt;- func() {
    // Send new func value in a new goroutine:
    // this will never block the monitor goroutine
    go func() {
        actions &lt;- 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 &lt;- func() {
    newfv := func() { /* do something */ }
    // First try to send with select:
    select {
    case actions &lt;- newfv:
        // Success!
    default:
        // Buffer is full, must do it in new goroutine:
        go func() {
            actions &lt;- 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 &lt;- fv:
		// Success!
	default:
		// Buffer is full, must do it in new goroutine:
		go func() {
			actions &lt;- fv
		}()
	}
}

And using it:

actions &lt;- func() {
    safeSend(func() {
        // something to do
    })
}

huangapple
  • 本文由 发表于 2016年2月27日 19:59:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/35669444.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定