【发布时间】:2021-11-11 02:46:45
【问题描述】:
这是上一个问题的后续问题,涵盖完全相同的主题Benefits of actor pattern in HTTP handler
下面我重复了该帖子中的代码:
func (a *API) handleNext(w http.ResponseWriter, r *http.Request) {
var (
notFound = make(chan struct{})
otherError = make(chan error)
nextID = make(chan string)
)
a.action <- func() {
s, err := a.log.Oldest()
if err == ErrNoSegmentsAvailable {
close(notFound)
return
}
if err != nil {
otherError <- err
return
}
id := uuid.New()
a.pending[id] = pendingSegment{s, time.Now().Add(a.timeout), false}
nextID <- id
}
select {
case <-notFound:
http.NotFound(w, r)
case err := <-otherError:
http.Error(w, err.Error(), http.StatusInternalServerError)
case id := <-nextID:
fmt.Fprint(w, id)
}
}
一个单独的 goroutine 在后台运行下面的循环来监听动作通道。所有的突变都发生在这里,因为 goroutine 具有独占访问权限,充当同步点:
func (a *API) loop() {
for {
select {
case f := <-a.action:
f()
}
}
}
原帖质疑这种模式的实用性,因为handleNext 底部的选择循环阻塞,直到发送到action chan 的函数被触发(在专用的loop goroutine 中),每次调用handleNext 串行运行。对原始问题的回答说明了“所有 goroutine 的总和”的总体好处,但我不确定我是否理解这种情况。
我目前的期望是,如果我们有 10 个客户端连接,每个客户端都调用 handleNext,它们都会立即被阻止,直到单个专用 loop 从 action chan 中提取一个项目。由于只有一个专用的 goroutine loop 用于触发动作,并且这些动作必须在下一个 handleNext goroutine 继续之前完全完成,因此永远不会有超过一个 handleNext 的并发执行。
我知道这种模式避免了锁定的需要,因为所有突变都将被限制在 loop goroutine 中,但它不是也阻止了多个客户端同时工作吗?如果在loop 内部,对f() 的调用是go f(),那么handleNext 函数将同时执行,但这会破坏模式的目的,因为那时你会回到需要使用锁在action 函数内。
我一定是误会了这里的东西。
所以 - 我可以看到我们有无锁同步作为这种模式的一个好处,但这不是以一次只处理一个客户端为代价的吗?如果这是真的,那么这与一次只处理一个请求、串行处理、没有锁或任何其他同步原语有什么不同?
【问题讨论】:
-
“这不是以一次只在一个客户端上工作为代价的吗”:在他的演讲中,他通过重写代码来说明这一部分,该代码一次也只运行一个动作。我猜他并不是在建议您按照这种正确的方式安装所有代码,他更多地提到他发现拥有
func()s 的单个频道而不是一堆频道fooReqC, barReqC, ... -
how would this be different than just handling one request at a time, serially, without locks or any other synchronization primitives?如果不使用通道或互斥锁,如何在同时调用的函数中实现它? -
@LeGEC 是的,我知道 - 早期的模式是相同的想法,只是每个动作都有一个通道,而不是一个处理所有动作的闭包通道。我想这个问题适用于这两种模式。我意识到这不是“一种真正的方式”,但如果它没有同时运行 api 函数,有什么好处?
-
@mh-cbon - 你一次只有一个 goroutine 处理 API 请求。