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

我正在阅读 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:结果就会不同步。


汪汪一只猫
浏览 90回答 1
1回答

浮云间

这已由评论回答(请参阅此处的 mkopriva 评论),但让我提供一个“答案化”的版本。首先,一个小问题:done&nbsp;:=&nbsp;make(chan&nbsp;interface{})我经常看到make(chan struct{})这里。由于从未发送任何实际值,因此通道的类型并不重要,但发送空struct值根本不占用空间,而发送空值则interface{}占用空间。1现在,我们要在这里做的,在一个闭包中,2是:等待(或至少假装等待)某个服务器回答;如果发生超时,停止等待服务器;和将我们的 ID 传递到结果通道或者如果done频道关闭(表明其他人在做任何事情上都比我们强),请不要理会上述任何事情。作为一个复杂因素,我们还将记录我们等待了多长时间,即使我们没有得到答案。主协程:创建done通道,其唯一目的是closed 以便从它接收立即在 EOF 处返回其缺少值的零值;衍生出一些(具体来说是 10 个)这些工作 goroutine;等待第一个传递结果(可能是由于超时导致的缺少结果,结果)关闭done通道以使剩余的工作人员终止;和打印最终结果。我们感兴趣的是为什么闭包的代码是用代码片段编写的:&nbsp;&nbsp;&nbsp;&nbsp;select&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;case&nbsp;<-done:&nbsp;&nbsp;&nbsp;&nbsp;case&nbsp;<-time.After(simulatedLoadTime): &nbsp;&nbsp;&nbsp;&nbsp;}在里面。这里的诀窍是预先select评估其所有备选方案。因此,它会在开始选择过程之前评估done通道,但也会调用。time.After()然后select等待任何一个具有值或处于通道末端并因此具有 EOF,以先发生者为准。如果还没有goroutine 将结果发送回主 goroutine,done则不会关闭通道。&nbsp;此时,所有goroutine 都会阻塞done通道。但是所有的 goroutine 也会调用time.After.该time.After代码启动了一个 goroutine,在一段时间后,它将在通道上发送当前时间。然后它返回该频道。因此,这两个操作中至少有一个<-将完成:done通道将关闭或关闭,并且由于 EOF,我们将在其上获得零值,或者返回的通道time.After将有一个时间发送给它,我们将收到该值。无论我们实际得到哪个值,我们都会将值放在地板上,但是两个<-运算符中的一个最终会解除阻塞的事实保证了这个 goroutine 最终能够继续进行。首先发生的事件将是done通道的关闭,或时间的接收。我们不知道这是哪一个,因为我们不知道done通道关闭需要多长时间,但是时间的上限是我们传递给的持续时间time.After。也就是说,要么done发生(最终),要么在我们选择的时间之后,time.After部分发生。其中之一肯定会发生。现在,如果我们不关心记录所花费的时间,我们可以这样写:&nbsp;&nbsp;&nbsp;&nbsp;select&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;case&nbsp;<-done:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return &nbsp;&nbsp;&nbsp;&nbsp;case&nbsp;<-time.After(simulatedLoadTime):&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;everything&nbsp;else&nbsp;happens&nbsp;here &nbsp;&nbsp;&nbsp;&nbsp;}但请注意原始代码中的注释://&nbsp;do&nbsp;not&nbsp;want&nbsp;to&nbsp;return&nbsp;on&nbsp;<-done&nbsp;because&nbsp;we&nbsp;still&nbsp;want&nbsp;to&nbsp;log&nbsp;...所以这就解释了缺少return.超时后,我们现在必须尝试将我们的 ID 发送到主 goroutine。但是,我们可能无法做到这一点:其他一些工作 goroutine 可能会在发送时击败我们,而主 goroutine 只从通道中读取一个值。为了确保我们不会被困在这里,我们还有另一个select.&nbsp;我们将尝试发送我们的 ID,但如果done通道现在或即将关闭,则停止。然后我们将记录并返回。1我一直认为 Go 应该有一个预先声明的空结构类型,作为一种方便和风格的东西。我们将在此处将其用于我们的done频道。我们会将其用于仅作为集合存在的地图,除了它们也将具有预先声明的仅方便和样式的类型。但这完全是另一回事。2这里没有特别好的理由使用闭包。未导出的普通函数也可以正常工作。鉴于我们正在使用闭包,我们可以捕获done通道、wg *WaitGroup值和result通道,而不是将它们作为参数。我不清楚为什么作者选择把它写成一个可能是一个函数的闭包,然后不理会闭包给我们带来的任何好处。
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Go