为了账号安全,请及时绑定邮箱和手机立即绑定

并发和复制请求,time.After() 是干什么用的?

并发和复制请求,time.After() 是干什么用的?

Go
汪汪一只猫 2022-07-25 10:37:29
我正在阅读 Go 中的并发性,并且非常接近尾声!总的来说,这是一本很棒的书。在其中一个示例中,作者正在描述如何模拟请求复制。代码示例是这样的:func main() {    doWork := func(        done <-chan interface{},        id int,        wg *sync.WaitGroup,        result chan<- int,    ) {        started := time.Now()        defer wg.Done()        // Simulate random load        simulatedLoadTime := time.Duration(1*rand.Intn(5)) * time.Second        /** use two separate select blocks because we want to send/receive two different values, the time.After (receive) and the id (send).        / if they were in the same select block, then we could only use one value at a time, the other will get lost. */        select {        // do not want to return on <-done because we still want to log the time it took        case <-done:        case <-time.After(simulatedLoadTime):        }        select {        case <-done:        case result <- id:        }        took := time.Since(started)        // Display how long handlers would have taken        if took < simulatedLoadTime {            took = simulatedLoadTime        }        fmt.Printf("%v took %v\n", id, took)    }    done := make(chan interface{})    result := make(chan int)    var wg sync.WaitGroup    wg.Add(10)    for i := 0; i < 10; i++ {        go doWork(done, i, &wg, result)    }    firstReturned := <-result    close(done)    wg.Wait()    fmt.Printf("Received an answer from #%v\n", firstReturned)}我不明白的一行是case <-time.After(simulatedLoadTime). 为什么会在这里?我们什么时候使用过从该通道返回的值。该通道甚至是如何在选择块之外进行通信的?无论出于何种原因,这条线似乎在同步结果的时间方面非常重要,因为如果我用 a 替换它,default:结果就会不同步。
查看完整描述

1 回答

?
浮云间

TA贡献1829条经验 获得超4个赞

这已由评论回答(请参阅此处的 mkopriva 评论),但让我提供一个“答案化”的版本。

首先,一个小问题:

done := make(chan interface{})

我经常看到make(chan struct{})这里。由于从未发送任何实际值,因此通道的类型并不重要,但发送空struct值根本不占用空间,而发送空值则interface{}占用空间。1

现在,我们要在这里做的,在一个闭包中,2是:

  • 等待(或至少假装等待)某个服务器回答;

  • 如果发生超时,停止等待服务器;和

  • 将我们的 ID 传递到结果通道

或者如果done频道关闭(表明其他人在做任何事情上都比我们强),请不要理会上述任何事情。

作为一个复杂因素,我们还将记录我们等待了多长时间,即使我们没有得到答案。

主协程:

  • 创建done通道,其唯一目的是closed 以便从它接收立即在 EOF 处返回其缺少值的零值;

  • 衍生出一些(具体来说是 10 个)这些工作 goroutine;

  • 等待第一个传递结果(可能是由于超时导致的缺少结果,结果)

  • 关闭done通道以使剩余的工作人员终止;和

  • 打印最终结果。

我们感兴趣的是为什么闭包的代码是用代码片段编写的:

    select {    case <-done:    case <-time.After(simulatedLoadTime):
    }

在里面。

这里的诀窍是预先select评估其所有备选方案。因此,它会在开始选择过程之前评估done通道,但也会调用。time.After()然后select等待任何一个具有值或处于通道末端并因此具有 EOF,以先发生者为准。

如果还没有goroutine 将结果发送回主 goroutine,done则不会关闭通道。 此时,所有goroutine 都会阻塞done通道。但是所有的 goroutine 也会调用time.After.

time.After代码启动了一个 goroutine,在一段时间后,它将在通道上发送当前时间。然后它返回该频道。因此,这两个操作中至少有一个<-将完成:done通道将关闭或关闭,并且由于 EOF,我们将在其上获得零值,或者返回的通道time.After将有一个时间发送给它,我们将收到该值。无论我们实际得到哪个值,我们都会将值放在地板上,但是两个<-运算符中的一个最终解除阻塞的事实保证了这个 goroutine 最终能够继续进行。

首先发生的事件将是done通道的关闭,或时间的接收。我们不知道这是哪一个,因为我们不知道done通道关闭需要多长时间,但是时间的上限是我们传递给的持续时间time.After。也就是说,要么done发生(最终),要么在我们选择的时间之后,time.After部分发生。其中之一肯定会发生。

现在,如果我们不关心记录所花费的时间,我们可以这样写:

    select {    case <-done:        return
    case <-time.After(simulatedLoadTime):        // everything else happens here
    }

但请注意原始代码中的注释:

// do not want to return on <-done because we still want to log ...

所以这就解释了缺少return.

超时后,我们现在必须尝试将我们的 ID 发送到主 goroutine。但是,我们可能无法做到这一点:其他一些工作 goroutine 可能会在发送时击败我们,而主 goroutine 只从通道中读取一个值。为了确保我们不会被困在这里,我们还有另一个select. 我们将尝试发送我们的 ID,但如果done通道现在或即将关闭,则停止。然后我们将记录并返回。


1我一直认为 Go 应该有一个预先声明的空结构类型,作为一种方便和风格的东西。我们将在此处将其用于我们的done频道。我们会将其用于仅作为集合存在的地图,除了它们也将具有预先声明的仅方便和样式的类型。但这完全是另一回事。

2这里没有特别好的理由使用闭包。未导出的普通函数也可以正常工作。鉴于我们正在使用闭包,我们可以捕获done通道、wg *WaitGroup值和result通道,而不是将它们作为参数。我不清楚为什么作者选择把它写成一个可能是一个函数的闭包,然后不理会闭包给我们带来的任何好处。


查看完整回答
反对 回复 2022-07-25
  • 1 回答
  • 0 关注
  • 89 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信