【问题标题】:How to ensure context listener routine finishes before main exits?如何确保上下文侦听器例程在主退出之前完成?
【发布时间】:2021-09-13 18:35:59
【问题描述】:

以下是我遇到的场景的简化示例。去游乐场here.

主要方法是定期调用process函数。进程函数获取一个锁,它必须在返回之前或在应用程序因中断而关闭之前释放。

通过取消传递给process 的上下文来处理中断。在我的示例中,我只是取消了上下文。

如何确保在取消上下文时解锁逻辑执行完成?

ATM,在我的测试中,逻辑没有被执行完成。例程已启动,但在程序退出之前似乎并未完成。

好像主方法正在退出,中途杀死例程。那可能吗?如何确保例程始终在程序退出之前完成?谢谢!

package main

import (
    "fmt"
    "context"
    "time"
    "sync"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    
    var wg sync.WaitGroup
    wg.Add(1)
    go func(){
        fmt.Println("Started periodic process")
        defer wg.Done()
        ticker := time.NewTicker(20 * time.Millisecond)
        for {
            select {
                case <-ticker.C:
                    process(ctx)
                case <-ctx.Done():
                    ticker.Stop()                   
    
            }
        }
        fmt.Println("Finished periodic process")
    }()
    
    time.Sleep(100 * time.Millisecond)  
    
    // cancel context
    cancel()
    
    wg.Wait()
}

func process(ctx context.Context) {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()
    
    go func() {
        <-ctx.Done()
        
        // unlock resources
        fmt.Println("Unlocking resources")

        time.Sleep(20 * time.Millisecond)

        fmt.Println("Unlocked resources")       
    }()
    
    // do some work
    fmt.Println("Started process")
    
    // acquire lock, in actual process
    // acquiring lock will fail unless
    // it was released be previous process
    // run
    fmt.Println("Acquired lock")
    
    time.Sleep(10 * time.Millisecond)
    
    fmt.Println("Finished process")

}



【问题讨论】:

标签: go


【解决方案1】:

就目前而言,您的程序将永远不会退出。这是因为在 main 开始的 goroutine 中的 for 循环永远不会退出(您在 ctx.Done() 时停止代码但不退出循环)。

第二个问题是,在process 中,goroutine 在函数退出时被取消(通过defer cancel()),但是由于延迟,goroutine 将继续运行一段时间。此处的解决方案取决于您是否需要异步进行“解锁”(我认为这并不重要)。

以下 (playground) 解决了这两个问题,并确保 process 在资源可用之前不会返回;如果您需要并行释放这些,那么一种选择是将WaitGroup 传递给函数)

package main

import (
    "fmt"
    "context"
    "time"
    "sync"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    
    var wg sync.WaitGroup
    wg.Add(1)
    go func(){
            fmt.Println("Started periodic process")
        defer wg.Done()
        ticker := time.NewTicker(20 * time.Millisecond)
        for {
            select {
                case <-ticker.C:
                    process(ctx)
                case <-ctx.Done():
                    ticker.Stop()                   
                    fmt.Println("Finished periodic process")
                    return
            }
        }       
    }() 
    time.Sleep(100 * time.Millisecond)  
    cancel()    
    wg.Wait()
}

func process(ctx context.Context) {
    ctx, cancel := context.WithCancel(ctx)
    done := make(chan struct{}) // could also use a waitgroup
    
    go func() {
        <-ctx.Done()
        fmt.Println("Unlocking resources")
        time.Sleep(10 * time.Millisecond)
        fmt.Println("Unlocked resources")   
        close(done) 
    }()
    

    // do some work
    fmt.Println("Started process")  
    time.Sleep(10 * time.Millisecond)   
    fmt.Println("Finished process")
    
    cancel()
    <-done // Wait for resources to be unlocked
}

【讨论】:

  • 感谢您发现永远不会退出 for 循环的错误,并展示了如何确保锁释放 goroutine 在进程返回之前完成 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-03-25
  • 2012-05-03
  • 1970-01-01
  • 2016-09-12
相关资源
最近更新 更多