是否可以取消未完成的 goroutines?

Is it possible to cancel unfinished goroutines?

考虑一组check作品,每个作品都有独立的逻辑,所以它们似乎对运行并发很好,比如:

type Work struct {
    // ...
}

// This Check could be quite time-consuming
func (w *Work) Check() bool {
    // return succeed or not

    //...
}

func CheckAll(works []*Work) {
    num := len(works)
    results := make(chan bool, num)
    for _, w := range works {
        go func(w *Work) {
            results <- w.Check()
        }(w)
    }

    for i := 0; i < num; i++ {
        if r := <-results; !r {
            ReportFailed()
            break;
        }
    }
}

func ReportFailed() {
    // ...
}

当关注results时,如果逻辑是无论哪一个工作失败,我们断言所有工作完全失败,通道中的剩余值是无用。让剩余未完成的 goroutines 继续 运行 并将结果发送到通道是没有意义和浪费的,尤其是当 w.Check() 相当耗时时。理想效果类似于:

    for _, w := range works {
        if !w.Check() {
            ReportFailed()
            break;
        }
    }

这只是 运行 必要的检查工作然后中断,但在顺序非并发情况下。

那么,是否可以取消这些未完成的goroutine,或者发送到频道?

正在取消(阻塞)发送

您原来的问题是问如何取消发送操作。频道上的发送基本上是“即时的”。如果通道的缓冲区已满并且没有准备好的接收器,则通道上的发送将阻塞。

可以 使用 select 语句和您关闭的 cancel 频道“取消”此发送,例如:

cancel := make(chan struct{})

select {
case ch <- value:
case <- cancel:
}

在另一个 goroutine 上使用 close(cancel) 关闭 cancel 通道将使上面的 select 放弃 ch 上的发送(如果它正在阻塞)。

但是如前所述,发送在“就绪”通道上是“即时的”,并且发送首先评估要发送的值:

results <- w.Check()

这首先必须 运行 w.Check(),完成后,其 return 值将在 results.

上发送

正在取消函数调用

所以你真正需要的是取消w.Check()方法调用。为此,惯用的方法是传递一个可以取消的 context.Context 值,并且 w.Check() 本身必须监视并“服从”这个取消请求。

请注意,您的函数必须明确支持这一点。没有函数调用或 goroutines 的隐式终止,参见

所以你的 Check() 应该看起来像这样:

// This Check could be quite time-consuming
func (w *Work) Check(ctx context.Context, workDuration time.Duration) bool {
    // Do your thing and monitor the context!

    select {
    case <-ctx.Done():
        return false
    case <-time.After(workDuration): // Simulate work
        return true
    case <-time.After(2500 * time.Millisecond): // Simulate failure after 2.5 sec
        return false
    }
}

CheckAll() 可能看起来像这样:

func CheckAll(works []*Work) {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    num := len(works)
    results := make(chan bool, num)

    wg := &sync.WaitGroup{}
    for i, w := range works {
        workDuration := time.Second * time.Duration(i)
        wg.Add(1)
        go func(w *Work) {
            defer wg.Done()
            result := w.Check(ctx, workDuration)
            // You may check and return if context is cancelled
            // so result is surely not sent, I omitted it here.
            select {
            case results <- result:
            case <-ctx.Done():
                return
            }
        }(w)
    }

    go func() {
        wg.Wait()
        close(results) // This allows the for range over results to terminate
    }()

    for result := range results {
        fmt.Println("Result:", result)
        if !result {
            cancel()
            break
        }
    }
}

正在测试:

CheckAll(make([]*Work, 10))

输出(在 Go Playground 上尝试):

Result: true
Result: true
Result: true
Result: false

我们 true 打印了 3 次(工作在 2.5 秒内完成),然后故障模拟开始,returns false,并终止所有其他工作。

请注意,上面示例中的 sync.WaitGroup 并不是严格需要的,因为 results 有一个能够保存所有结果的缓冲区,但总的来说这仍然是一个好习惯(如果你使用较小的缓冲区将来)。

查看相关内容:

package main

import "fmt"

type Work struct {
    // ...
    Name string
    IsSuccess chan bool
}

// This Check could be quite time-consuming
func (w *Work) Check() {
    // return succeed or not

    //...
    if len(w.Name) > 0 {
        w.IsSuccess <- true
    }else{
        w.IsSuccess <- false
    }

}


//堆排序
func main() {
    works := make([]*Work,3)
    works[0] = &Work{
        Name: "",
        IsSuccess: make(chan bool),
    }
    works[1] =  &Work{
        Name: "111",
        IsSuccess: make(chan bool),
    }
    works[2] =&Work{
        Name: "",
        IsSuccess: make(chan bool),
    }

    for _,w := range works {
        go w.Check()
    }

    for i,w := range works{
        select {
        case checkResult := <-w.IsSuccess :
            fmt.Printf("index %d checkresult %t \n",i,checkResult)
        }
    }
}

enter image description here

简短的回答是:

您不能取消或关闭任何 goroutine,除非 goroutine 本身到达 return 或其堆栈的末尾。

如果你想取消某些东西,最好的方法是传递一个 context.Context 给他们,然后在例程中听这个 context.Done()。每当上下文被取消时,你应该 return 并且 goroutine 将在执行延迟(如果有)后自动死亡。