用于取消飞行中例程的golang设计模式

clo*_*jur 6 go

我是一位golang新手,正在尝试了解此问题的正确设计模式。我当前的解决方案似乎冗长,我不确定哪种更好的方法。

我正在尝试设计一个系统,该系统:

  1. 执行N个例程
  2. 尽快返回每个goroutine的结果
  3. 如果goroutine返回特定值,则应该杀死其他goroutine将取消。

目标:我想开始一些goroutine,但是如果一个例程返回特定结果,我想取消这些例程。

我试图了解我的代码是否超级“臭”,或者这是否是规定的处理方式。我仍然感觉不太好,因此我们将不胜感激。

这是我写的:

package main

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

func main() {

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

    fooCheck := make(chan bool)
    barCheck := make(chan bool)

    go foo(ctx, 3000, fooCheck)
    go bar(ctx, 5000, barCheck)

    for fooCheck != nil ||
        barCheck != nil {
        select {
        case res, ok := <-fooCheck:
            if !ok {
                fooCheck = nil
                continue
            }
            if res == false {
                cancel()
            }
            fmt.Printf("result of foocheck: %t\n", res)
        case res, ok := <-barCheck:
            if !ok {
                barCheck = nil
                continue
            }
            fmt.Printf("result of barcheck: %t\n", res)
        }
    }
    fmt.Printf("here we are at the end of the loop, ready to do some more processing...")
}

func foo(ctx context.Context, pretendWorkTime int, in chan<- bool) {
    fmt.Printf("simulate doing foo work and pass ctx down to cancel down the calltree\n")
    time.Sleep(time.Millisecond * time.Duration(pretendWorkTime))

    select {
    case <-ctx.Done():
        fmt.Printf("\n\nWe cancelled this operation!\n\n")
        break
    default:
        fmt.Printf("we have done some foo work!\n")
        in <- false
    }
    close(in)
}

func bar(ctx context.Context, pretendWorkTime int, in chan<- bool) {
    fmt.Printf("simulate doing bar work and pass ctx down to cancel down the calltree\n")
    time.Sleep(time.Millisecond * time.Duration(pretendWorkTime))

    select {
    case <-ctx.Done():
        fmt.Printf("\n\nWe cancelled the bar operation!\n\n")
        break
    default:
        fmt.Printf("we have done some bar work!\n")
        in <- true
    }
    close(in)
}
Run Code Online (Sandbox Code Playgroud)

(在此处使用代码进行播放:https : //play.golang.org/p/HAA-LIxWNt0

输出按预期方式工作,但恐怕我正在做出一些决定,这些决定以后会引起我的注意。

icz*_*cza 3

我会使用单个通道来传达结果,因此收集结果要容易得多,并且它会根据其性质自动“扩展”。如果您需要识别结果的来源,只需使用包含源的包装器即可。像这样的东西:

type Result struct {
    ID     string
    Result bool
}
Run Code Online (Sandbox Code Playgroud)

为了模拟“真实”工作,工作人员应该使用循环以迭代方式完成工作,并且在每次迭代中他们应该检查取消信号。像这样的东西:

func foo(ctx context.Context, pretendWorkMs int, resch chan<- Result) {
    log.Printf("foo started...")
    for i := 0; i < pretendWorkMs; i++ {
        time.Sleep(time.Millisecond)
        select {
        case <-ctx.Done():
            log.Printf("foo terminated.")
            return
        default:
        }
    }
    log.Printf("foo finished")
    resch <- Result{ID: "foo", Result: false}
}
Run Code Online (Sandbox Code Playgroud)

在我们的示例中,bar()只需将所有foo单词替换为即可bar

现在执行作业并在满足我们期望的情况下提前终止其余作业,如下所示:

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

resch := make(chan Result, 2)

log.Println("Kicking off workers...")
go foo(ctx, 3000, resch)
go bar(ctx, 5000, resch)

for i := 0; i < cap(resch); i++ {
    result := <-resch
    log.Printf("Result of %s: %v", result.ID, result.Result)
    if !result.Result {
        cancel()
        break
    }
}
log.Println("Done.")
Run Code Online (Sandbox Code Playgroud)

运行此应用程序将输出(在Go Playground上尝试):

2009/11/10 23:00:00 Kicking off workers...
2009/11/10 23:00:00 bar started...
2009/11/10 23:00:00 foo started...
2009/11/10 23:00:03 foo finished
2009/11/10 23:00:03 Result of foo: false
2009/11/10 23:00:03 Done.
Run Code Online (Sandbox Code Playgroud)

有些事情需要注意。如果由于意外结果而提前终止,该cancel()函数将被调用,并且我们会跳出循环。其余的工作人员可能也会同时完成他们的工作并发送他们的结果,这不会成为问题,因为我们使用了缓冲通道,因此他们的发送不会阻塞并且他们将正确结束。另外,如果它们没有同时完成,它们会检查ctx.Done()循环,并提前终止,因此 goroutine 会被很好地清理。

另请注意,上述代码的输出不会打印bar terminated。这是因为main()函数在循环之后立即终止,并且一旦main()函数结束,它就不会等待其他非maingoroutine 完成。详细信息请参见Go 中 goroutine 无输出。如果应用程序不会立即终止,我们也会看到该行被打印出来。time.Sleep()如果我们在末尾添加一个main()

log.Println("Done.")
time.Sleep(3 * time.Millisecond)
Run Code Online (Sandbox Code Playgroud)

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

2009/11/10 23:00:00 Kicking off workers...
2009/11/10 23:00:00 bar started...
2009/11/10 23:00:00 foo started...
2009/11/10 23:00:03 foo finished
2009/11/10 23:00:03 Result of foo: false
2009/11/10 23:00:03 Done.
2009/11/10 23:00:03 bar terminated.
Run Code Online (Sandbox Code Playgroud)

现在,如果您必须等待所有工作人员“正常”或“提前”结束才能继续,您可以通过多种方式实现这一点。

一种方法是使用sync.WaitGroup. 有关示例,请参阅在 Golang 中防止 main() 函数在 goroutine 完成之前终止。另一种方法是让每个工作人员发送 ,Result无论它们如何结束,并且Result可以包含终止条件,例如normalor aborted。Goroutinemain()可以继续接收循环,直到n从 接收到值resch。如果选择此解决方案,您必须确保每个工作人员发送一个值(即使发生恐慌),以免main()在这种情况下阻塞(例如使用 using defer)。