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

时间:2019-05-17 22:42:29

标签: go

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

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

  1. 执行N个goroutines
  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)
}

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

输出效果正常,但恐怕我正在做出一些决定,这些决定以后会引起我的注意。

2 个答案:

答案 0 :(得分:2)

我将使用单个渠道来传达结果,因此收集结果要容易得多,并且它会根据其性质自动“缩放”。如果您需要确定结果的来源,只需使用包含该来源的包装器即可。像这样:

type Result struct {
    ID     string
    Result bool
}

要模拟“真实”工作,工作人员应使用循环以迭代方式进行工作,并且在每次迭代中,他们应检查取消信号。像这样:

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}
}

在我们的示例中,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.")

运行此应用程序将输出(在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.

一些注意事项。如果由于意外结果而提前终止,则会调用cancel()函数,并从循环中退出。可能是其余的工作人员也同时完成了他们的工作并发送了结果,这在我们使用缓冲通道的情况下不会有问题,因此他们的发送不会阻塞并且可以正常结束。另外,如果没有同时完成,则在循环中检查ctx.Done(),并提早终止,因此可以很好地清理goroutine。

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

log.Println("Done.")
time.Sleep(3 * time.Millisecond)

输出将是(在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.

现在,如果您必须等待所有工作人员“正常”或“提前”结束,然后再继续前进,则可以通过多种方式实现这一目标。

一种方法是使用sync.WaitGroup。有关示例,请参见Prevent the main() function from terminating before goroutines finish in Golang。另一种方法是让每个工作人员无论他们如何结束都发送Result,并且Result可以包含终止条件,例如normalaborted。并且main() goroutine可以继续接收循环,直到它从n接收到resch个值为止。如果选择了此解决方案,则必须确保每个工作程序都发送一个值(即使发生紧急情况),以在这种情况下(例如,使用main())不会阻塞defer

答案 1 :(得分:-1)

对于您所讨论的内容,我将分享最简单的模式。您可以将其扩展到更复杂的情况。

func doStuff() {
    // This can be a chan of anything.
    msgCh := make(chan string)

    // This is how you tell your go-routine(s) to stop, by closing this chan.
    quitCh := make(chan struct{})
    defer close(quitCh)

    // Start all go routines.
    for whileStart() {
        go func() {
            // Do w/e you need inside of your go-routine.

            // Write back the result.
            select {
            case msgCh <- "my message":
                // If we got here then the chan is open.
            case <-quitCh:
                // If we got here then the quit chan was closed.
            }
        }()
    }

    // Wait for all go routines.
    for whileWait() {
        // Block until a msg comes back.
        msg := <-msgCh
        // If you found what you want.
        if msg == stopMe {
            // It's safe to return because of the defer earlier.
            return
        }
    }
}