我是一位golang新手,正在尝试了解此问题的正确设计模式。我目前的解决方案似乎很冗长,我不确定哪种更好的方法。
我正在尝试设计一个系统,该系统:
目标:我想启动许多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)
输出效果正常,但恐怕我正在做出一些决定,这些决定以后会引起我的注意。
答案 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
可以包含终止条件,例如normal
或aborted
。并且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
}
}
}