如果在一个中发生错误,请关闭多个goroutine

时间:2017-08-04 07:37:37

标签: go error-handling synchronization exit goroutine

考虑这个功能:

func doAllWork() error {
    var wg sync.WaitGroup
    wg.Add(3)
    for i := 0; i < 2; i++ {
        go func() {
            defer wg.Done()
            for j := 0; j < 10; j++ {
                result, err := work(j)
                if err != nil {
                    // can't use `return err` here
                    // what sould I put instead ? 
                    os.Exit(0)
                }
            }
        }()
    }
    wg.Wait()
    return nil
}

在每个goroutine中,函数work()被调用10次。如果对work()的一次调用在任何正在运行的goroutine中返回错误,我希望所有goroutine立即停止,并退出程序。 可以在这里使用os.Exit()吗?我该怎么处理?

修改:此问题与how to stop a goroutine不同,因为如果在一个

中发生错误,我需要关闭所有goroutine

3 个答案:

答案 0 :(得分:20)

您可以使用为此类内容创建的context包(&#34;带有截止日期,取消信号......&#34; )。

您创建了一个能够使用context.WithCancel()发布取消信号的上下文(父上下文可能是context.Background()返回的上下文)。这将返回一个cancel()函数,该函数可用于取消(或更准确地说信号取消意图)给工作人员g。 并且在工作器goroutines中,您必须通过检查Context.Done()返回的通道是否已关闭来检查是否已启动此类意图,最简单的方法是尝试从中接收(如果它已关闭则立即进行)。要进行非阻塞检查(如果未关闭则可以继续),请将select语句与default分支一起使用。

我将使用以下work()实现,它模拟10%的失败几率,并模拟1秒的工作:

func work(i int) (int, error) {
    if rand.Intn(100) < 10 { // 10% of failure
        return 0, errors.New("random error")
    }
    time.Sleep(time.Second)
    return 100 + i, nil
}

doAllWork()可能如下所示:

func doAllWork() error {
    var wg sync.WaitGroup

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // Make sure it's called to release resources even if no errors

    for i := 0; i < 2; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()

            for j := 0; j < 10; j++ {
                // Check if any error occurred in any other gorouties:
                select {
                case <-ctx.Done():
                    return // Error somewhere, terminate
                default: // Default is must to avoid blocking
                }
                result, err := work(j)
                if err != nil {
                    fmt.Printf("Worker #%d during %d, error: %v\n", i, j, err)
                    cancel()
                    return
                }
                fmt.Printf("Worker #%d finished %d, result: %d.\n", i, j, result)
            }
        }(i)
    }
    wg.Wait()

    return ctx.Err()
}

这是如何测试的:

func main() {
    rand.Seed(time.Now().UnixNano() + 1) // +1 'cause Playground's time is fixed
    fmt.Printf("doAllWork: %v\n", doAllWork())
}

输出(在Go Playground上尝试):

Worker #0 finished 0, result: 100.
Worker #1 finished 0, result: 100.
Worker #1 finished 1, result: 101.
Worker #0 finished 1, result: 101.
Worker #0 finished 2, result: 102.
Worker #1 finished 2, result: 102.
Worker #1 finished 3, result: 103.
Worker #1 during 4, error: random error
Worker #0 finished 3, result: 103.
doAllWork: context canceled

如果没有错误,例如使用以下work()函数时:

func work(i int) (int, error) {
    time.Sleep(time.Second)
    return 100 + i, nil
}

输出就像(在Go Playground上试试):

Worker #0 finished 0, result: 100.
Worker #1 finished 0, result: 100.
Worker #1 finished 1, result: 101.
Worker #0 finished 1, result: 101.
Worker #0 finished 2, result: 102.
Worker #1 finished 2, result: 102.
Worker #1 finished 3, result: 103.
Worker #0 finished 3, result: 103.
Worker #0 finished 4, result: 104.
Worker #1 finished 4, result: 104.
Worker #1 finished 5, result: 105.
Worker #0 finished 5, result: 105.
Worker #0 finished 6, result: 106.
Worker #1 finished 6, result: 106.
Worker #1 finished 7, result: 107.
Worker #0 finished 7, result: 107.
Worker #0 finished 8, result: 108.
Worker #1 finished 8, result: 108.
Worker #1 finished 9, result: 109.
Worker #0 finished 9, result: 109.
doAllWork: <nil>

备注:

基本上我们只使用了上下文的Done()频道,所以看起来我们可以轻松(如果不是更容易)使用done频道而不是Context,关闭在上述解决方案中执行cancel()所做的工作的渠道。

事实并非如此。 这只能在只有一个goroutine关闭频道的情况下使用,但在我们的情况下,任何工作人员都可以这样做。并尝试关闭已经关闭的频道恐慌(详见此处:{{3 }})。因此,您必须确保在close(done)周围进行某种同步/排除,这会降低其可读性,甚至更复杂。实际上,这正是cancel()函数在引擎盖下所做的事情,隐藏/抽象远离你的眼睛,因此可以多次调用cancel()以使你的代码/使用它更简单。

如何从工人那里获取并返回错误?

为此,您可以使用错误频道:

errs := make(chan error, 2) // Buffer for 2 errors

当遇到错误时,在工作人员内部,将其发送到通道而不是打印它:

result, err := work(j)
if err != nil {
    errs <- fmt.Errorf("Worker #%d during %d, error: %v\n", i, j, err)
    cancel()
    return
}

在循环之后,如果出现错误,请返回(否则nil):

// Return (first) error, if any:
if ctx.Err() != nil {
    return <-errs
}
return nil

此次输出(在How does a non initialized channel behave?上尝试此操作):

Worker #0 finished 0, result: 100.
Worker #1 finished 0, result: 100.
Worker #1 finished 1, result: 101.
Worker #0 finished 1, result: 101.
Worker #0 finished 2, result: 102.
Worker #1 finished 2, result: 102.
Worker #1 finished 3, result: 103.
Worker #0 finished 3, result: 103.
doAllWork: Worker #1 during 4, error: random error

请注意,我使用的缓冲通道的缓冲区大小等于worker的数量,这可确保在其上发送始终是非阻塞的。这也使您可以接收和处理所有错误,而不仅仅是一个(例如第一个)。另一个选择可能是使用缓冲通道仅保持1,并对其执行非阻塞发送,这可能如下所示:

errs := make(chan error, 1) // Buffered only for the first error

// ...and inside the worker:

result, err := work(j)
if err != nil {
    // Non-blocking send:
    select {
    case errs <- fmt.Errorf("Worker #%d during %d, error: %v\n", i, j, err):
    default:
    }
    cancel()
    return
}

答案 1 :(得分:4)

一种更清晰的选择是使用errgroupdocumentation)。

程序包errgroup为处理共同任务的子任务的goroutine组提供同步,错误传播和上下文取消。

您可以在此示例(playground)中进行检查:

    var g errgroup.Group
    var urls = []string{
        "http://www.golang.org/",
        "http://www.google.com/",
        "http://www.somestupidname.com/",
    }

    for _, url := range urls {
        // Launch a goroutine to fetch the URL.
        url := url // https://golang.org/doc/faq#closures_and_goroutines
        
       g.Go(func() error {
            // Fetch the URL.
            resp, err := http.Get(url)
            if err == nil {
                resp.Body.Close()
            }
            return err
        })
    }
   
    // Wait for all HTTP fetches to complete.
    if err := g.Wait(); err == nil {
        fmt.Println("Successfully fetched all URLs.")
    
    } else {

        // After all have run, at least one of them has returned an error!
       // But all have to finish their work!
       // If you want to stop others goroutines when one fail, go ahead reading!
        fmt.Println("Unsuccessfully fetched URLs.")
    }

但是要注意:Go documentation中的The first call to return a non-nil error cancels the group短语有点误导。

实际上,errgroup.Group 如果是使用上下文创建的(WithContext函数),则在执行goroutine时将调用WithContext返回的上下文的cancel函数该组中的错误将返回错误,否则将不执行任何操作(read the source code here!)。

因此,如果要关闭不同的goroutine,则必须使用返回的WithContext上下文并由您自己在其中进行管理,errgroup只会关闭该上下文! Here you can find an example.

总而言之,errgroup可以以不同的方式使用,如examples所示。

  1. “只是错误”,如上例所示: Wait等待所有goroutine结束,然后返回第一个非空错误(如果有),或者返回nil

  2. 并行: 您必须使用WithContext function创建组,并使用上下文来管理上下文关闭。 I created a playground example here with some sleeps! 您必须手动关闭每个goroutine,但是使用上下文可以在关闭上下文时结束它们。

  3. 管道(请参见examples中的更多信息)。

答案 2 :(得分:0)

这里的另一种方法是使用errgroup.WithContext。您可以在此example中进行检查。

简而言之,g.Wait()等待第一个错误发生或等待所有错误完成。如果任何goroutine中发生错误(在提供的示例中超时),它将通过ctx.Done()通道取消其他goroutine中的执行。