【问题标题】:Golang Actor PatternGolang 演员模式
【发布时间】: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 请求。

标签: go actor goroutine


【解决方案1】:

请注意,匿名函数可以启动一个 goroutine 并提前返回:

    a.action <- func() {
        gizmo, err := a.checkGizmoAvailable() // this is run synchronously
        if err != nil {
           otherErr <- err
           return
        }

        // the remainder is run asynchronously
        go func(){
            s, err := gizmo.log.Oldest()
            if err == ErrNoSegmentsAvailable {
                close(notFound)
                return
            }
            if err != nil {
                otherError <- err
                return
            }
            id := uuid.New()
            
            // this will require synchronization :
            a.pending[id] = pendingSegment{s, time.Now().Add(a.timeout), false}
            nextID <- id
        }()

        // returning here, a.loop() can start the next function
    }

【讨论】:

  • 好的,这基本上就是我在上面第 3 段中提到的场景(在问题的底部),对吧?如果没有在单独的 goroutine(或调用一个)中运行操作,整个事情将是同步的,一次不会处理多个请求。这里我们引入了异步来允许在多个客户端上工作,但是现在我们必须再次使用锁——对吗?我不确定演讲中提出的动作函数是否假定在单独的 goroutine(或调用一个)中运行,因为它没有在 sn-p 中显示 - 我可能应该查找完整的源代码。
  • yes : 如果你的代码允许并发访问某些东西,你需要选择一种同步方式。
  • 对 - 所以原来的问题仍然存在。如果这种模式的主要好处是无锁同步,那么在不同的 goroutine 中运行 checkGizmoAvailable() 会破坏意图,因为我们回到了锁。如果我们不在一个单独的 goroutine 中运行它,那这不就等同于只在一个 goroutine 中运行所有东西而没有所有的通道杂技吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-03-07
  • 2016-07-29
  • 1970-01-01
  • 2019-05-10
  • 1970-01-01
相关资源
最近更新 更多