如何以惯用方式正常关闭已链接的goroutines

时间:2019-04-05 12:19:26

标签: go concurrency goroutine

创建多个goroutine,它们将在以多级方式进行处理时嵌套goroutine(想象一棵goroutine树,每个级别可以有很多叶子)。

按顺序正常关闭这些goroutine 并等待它们回来的惯用方式是什么?顺序是最下面的(最深的孩子在最前面),并且还假设我不知道我会事先(动态)启动多少个goroutine。

下面的示例仅以无序方式正常关闭了它们。

package main

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

func main() {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)

    //level1
    go func() {
        fmt.Println("level1 started")
        //level2
        go func() {
            fmt.Println("level2 started")

            //level3
            go func() {
                fmt.Println("level3 started")
                select {
                case <-ctx.Done():
                    fmt.Println("Done called on level3")
                case <-time.After(5* time.Second):
                    fmt.Println("After called on level3")
                }

            }()
            select {
            case <-ctx.Done():
                fmt.Println("Done called on level2")
            case <-time.After(7* time.Second):
                fmt.Println("After called on level2")
            }

        }()
        select {
        case <-ctx.Done():
            fmt.Println("Done called on level1")
        case <-time.After(10* time.Second):
            fmt.Println("After called on level1")
        }


    }()
    time.Sleep(1*time.Second)
    cancel()
    time.Sleep(1 * time.Second)
}

2 个答案:

答案 0 :(得分:2)

要等待一组goroutine,sync.WaitGroup是惯用的解决方案。您可以在启动新的goroutine(WaitGroup.Add())时为其计数器加1,并且goroutine可以发出信号,表明已使用WaitGroup.Done()完成。父goroutine可以调用WaitGroup.Wait()以等待其所有子项完成。

您可以在每个级别上执行相同的操作。在启动子级goroutine的每个级别上创建一个WaitGroup,并且仅在该goroutine的Wait()返回时返回。

这是在您的示例中的应用方式:

ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)

//level1
wg1 := &sync.WaitGroup{}
wg1.Add(1)
go func() {
    defer wg1.Done()
    fmt.Println("level1 started")
    //level2
    wg2 := &sync.WaitGroup{}
    wg2.Add(1)
    go func() {
        defer wg2.Done()
        fmt.Println("level2 started")

        //level3
        wg3 := &sync.WaitGroup{}
        wg3.Add(1)
        go func() {
            defer wg3.Done()
            fmt.Println("level3 started")
            select {
            case <-ctx.Done():
                fmt.Println("Done called on level3")
            case <-time.After(5 * time.Second):
                fmt.Println("After called on level3")
            }
            fmt.Println("Level 3 ended.")
        }()

        select {
        case <-ctx.Done():
            fmt.Println("Done called on level2")
        case <-time.After(7 * time.Second):
            fmt.Println("After called on level2")
        }
        wg3.Wait()
        fmt.Println("Level 2 ended.")
    }()

    select {
    case <-ctx.Done():
        fmt.Println("Done called on level1")
    case <-time.After(10 * time.Second):
        fmt.Println("After called on level1")
    }
    wg2.Wait()
    fmt.Println("Level 1 ended.")
}()

time.Sleep(1 * time.Second)
cancel()
wg1.Wait()
fmt.Println("Main ended.")

这将输出(在Go Playground上尝试):

level1 started
level2 started
level3 started
Done called on level1
Done called on level3
Level 3 ended.
Done called on level2
Level 2 ended.
Level 1 ended.
Parent ended.

输出中的重要内容是

Level 3 ended.
Level 2 ended.
Level 1 ended.
Main ended.

级别以降序顺序(从下至上)结束,以"Main ended."结尾。

答案 1 :(得分:0)

一种可能的方法,我也说惯用的,方法是通过strict{}通道。每当您希望终止所述goroutine时,只需向此通道shutdown <- struct{}{}中写入一个空结构即可。这应该做的工作。 或者,您可以关闭通道,通过将false作为<-的第二个返回值来识别它,但是我建议仅在需要与多个goroutine共享此通道时才使用它。总的来说,我发现这种方法有点伪劣和容易出错。

附带说明:在您的示例中,关闭goroutine的方式是,一旦上下文被取消,所有goroutine将返回。不知道在一般情况下这是否有很多好处。也许就您而言。