当通道关闭时,以接收通道为参数的goroutine是否停止?

时间:2018-12-07 16:47:21

标签: go concurrency goroutine channels

我一直在阅读“使用go构建微服务”,该书介绍了apache/go-resiliency/deadline软件包来处理超时。

deadline.go

// Package deadline implements the deadline (also known as "timeout") resiliency pattern for Go.
package deadline

import (
    "errors"
    "time"
)

// ErrTimedOut is the error returned from Run when the deadline expires.
var ErrTimedOut = errors.New("timed out waiting for function to finish")

// Deadline implements the deadline/timeout resiliency pattern.
type Deadline struct {
    timeout time.Duration
}

// New constructs a new Deadline with the given timeout.
func New(timeout time.Duration) *Deadline {
    return &Deadline{
        timeout: timeout,
    }
}

// Run runs the given function, passing it a stopper channel. If the deadline passes before
// the function finishes executing, Run returns ErrTimeOut to the caller and closes the stopper
// channel so that the work function can attempt to exit gracefully. It does not (and cannot)
// simply kill the running function, so if it doesn't respect the stopper channel then it may
// keep running after the deadline passes. If the function finishes before the deadline, then
// the return value of the function is returned from Run.
func (d *Deadline) Run(work func(<-chan struct{}) error) error {
    result := make(chan error)
    stopper := make(chan struct{})

    go func() {
        result <- work(stopper)
    }()

    select {
    case ret := <-result:
        return ret
    case <-time.After(d.timeout):
        close(stopper)
        return ErrTimedOut
    }
}

deadline_test.go

package deadline

import (
    "errors"
    "testing"
    "time"
)

func takesFiveMillis(stopper <-chan struct{}) error {
    time.Sleep(5 * time.Millisecond)
    return nil
}

func takesTwentyMillis(stopper <-chan struct{}) error {
    time.Sleep(20 * time.Millisecond)
    return nil
}

func returnsError(stopper <-chan struct{}) error {
    return errors.New("foo")
}

func TestDeadline(t *testing.T) {
    dl := New(10 * time.Millisecond)

    if err := dl.Run(takesFiveMillis); err != nil {
        t.Error(err)
    }

    if err := dl.Run(takesTwentyMillis); err != ErrTimedOut {
        t.Error(err)
    }

    if err := dl.Run(returnsError); err.Error() != "foo" {
        t.Error(err)
    }

    done := make(chan struct{})
    err := dl.Run(func(stopper <-chan struct{}) error {
        <-stopper
        close(done)
        return nil
    })
    if err != ErrTimedOut {
        t.Error(err)
    }
    <-done
}

func ExampleDeadline() {
    dl := New(1 * time.Second)

    err := dl.Run(func(stopper <-chan struct{}) error {
        // do something possibly slow
        // check stopper function and give up if timed out
        return nil
    })

    switch err {
    case ErrTimedOut:
        // execution took too long, oops
    default:
        // some other error
    }
}

第一个问题

// in deadline_test.go
if err := dl.Run(takesTwentyMillis); err != ErrTimedOut {
    t.Error(err)
}  

我在理解上述代码的执行流程时遇到问题。据我了解,因为takesTwentyMillis函数的睡眠时间比设置的10毫秒超时时间长,

// in deadline.go
case <-time.After(d.timeout):
    close(stopper)
    return ErrTimedOut

time.After发出当前时间,并选择这种情况。然后,关闭塞子通道并返回ErrTimeout。

我不明白的是, 关闭塞子通道会对可能仍在运行的匿名goroutine起作用 我认为,当塞子通道关闭时,以下goroutine可能仍在运行。

go func() {
        result <- work(stopper)
    }()

(如果我在这里错了,请纠正我)我想在close(stopper)之后,此goroutine将调用takesTwentyMillis(=工作函数),其停止通道为参数。函数将继续运行并睡眠20毫秒,然后返回nil传递到结果通道。并且main()在这里结束,对吧?

我看不到此处关闭塞子通道的意义。 takesTwentyMillis函数似乎并没有使用函数体内的通道:(。

第二个问题

// in deadline_test.go within TestDeadline()
done := make(chan struct{})
err := dl.Run(func(stopper <-chan struct{}) error {
    <-stopper
    close(done)
    return nil
})
if err != ErrTimedOut {
    t.Error(err)
}
<-done

这是我不完全理解的部分。我认为运行dl.Run时,将初始化塞子通道。但是因为停止器通道中没有值,所以该函数调用将在<-stopper处被阻塞...但是由于我不理解此代码,因此我不明白为什么此代码最初存在(即该代码正在尝试进行测试以及如何执行等)。


关于第二个问题的第三个(附加)问题

因此我了解到,当第二个问题中的Run函数触发塞子通道关闭时,worker函数将获得信号。然后工人关闭完成的通道并返回nil。 我使用delve(= go调试器)进行了查看,gdb将我带到deadline.go行之后return nil中的goroutine。

   err := dl.Run(func(stopper <-chan struct{}) error {
        <-stopper
        close(done)
-->     return nil   
    })

键入n进入下一行后,delve将我带到这里

    go func() {
-->             result <- work(stopper)
            }()

到此进程就结束了,因为当我再次输入n时,命令行会提示PASS并退出进程。为什么流程在这里完成? work(stopper)似乎返回nil,然后应该将其传递给结果通道吗?但是由于某些原因,该行似乎没有执行。

我知道主要的goroutine,即Run函数,已经返回了ErrTimedOut。所以我想这与此有关吗?

1 个答案:

答案 0 :(得分:1)

第一个问题

使用stopper通道是为了向功能发出信号,例如takesTwentyMillis已到了截止日期,呼叫者不再在乎其结果。通常,这意味着诸如takesTwentyMillis之类的辅助函数应检查stopper通道是否已关闭,以便可以取消其工作。尽管如此,检查stopper通道仍然是工作程序功能的选择。它可能会也可能不会检查频道。

func takesTwentyMillis(stopper <-chan struct{}) error {
    for i := 0; i < 20; i++ {
        select {
        case <-stopper:
            // caller doesn't care anymore might as well stop working
            return nil
        case <-time.After(time.Second): // simulating work
        }
    }
    // work is done
    return nil
}

第二个问题

Deadline.Run()的这一部分将关闭塞子通道。

case <-time.After(d.timeout):
    close(stopper)

在关闭的通道(<-stopper)上读取将立即为该通道返回零值。我认为这只是测试一个工作者功能,最终超时。