Golang 演员模式

Posted

技术标签:

【中文标题】Golang 演员模式【英文标题】:Golang Actor Pattern 【发布时间】: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] = pendingSegments, 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,它们都会立即被阻止,直到单个专用 loopaction 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 请求。 【参考方案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] = pendingSegments, 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 中运行所有东西而没有所有的通道杂技吗?

以上是关于Golang 演员模式的主要内容,如果未能解决你的问题,请参考以下文章

golang Golang Builder模式

golang Golang Regexp模式信息

手撸golang 行为型设计模式 委派模式

手撸golang 结构型设计模式 桥接模式

golang协程调度模式解密

手撸golang 行为型设计模式 状态模式