Параллелизм и реплицированные запросы, для чего нужен 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). Почему это здесь? Когда мы когда-либо использовали значение, возвращаемое из этого канала. Как этот канал вообще передается за пределы блока выбора? По какой-то причине эта строка кажется довольно неотъемлемой частью синхронизации результатов, потому что, если бы я заменил ее на default:, результаты не синхронизировались.

«Когда мы когда-либо использовали значение, возвращаемое из этого канала». Никогда не используется операция приема, а не получаемое значение. «Как этот канал вообще передается за пределы блока выбора?» Канал не сообщается. Однако получение сообщения приводит к тому, что select перестает блокироваться. select блокируется до тех пор, пока либо done не получит сообщение, либо time.After(simulatedLoadTime) не получит сообщение. Если вы добавите default, то select вообще не будет блокироваться.

mkopriva 25.12.2020 09:40
golang.org/ref/spec#Select_statements «2. Если одно или несколько сообщений могут продолжаться, одно из них выбирается с помощью универсального псевдослучайного выбора. В противном случае, если есть default случай, выбран этот случай. Если нет случая по умолчанию, оператор «выбрать» блокируется до тех пор, пока не будет продолжено хотя бы одно из сообщений».
mkopriva 25.12.2020 09:49

@mkopriva это круто, спасибо за разъяснение! Я посмотрел на gobyexample.com/non-blocking-channel-operations. Означает ли это, что time.After фактически не имеет отношения к процессу? Какой смысл иметь два отдельных блока выбора, если мы никогда не используем значение? Второй блок выбора имеет смысл как единственный блок, поскольку он также обрабатывает <-done

Chris Ngo 25.12.2020 09:53

Если <-done слишком долго выполняет свою работу, time.After можно использовать в качестве альтернативного пути. Например, «лимит» того, как долго select должен ждать другого cases. (Я не знаю, как именно он используется в коде вопроса или ссылке в комментарии, так как я только просмотрел код и не понимаю его назначения).

mkopriva 25.12.2020 09:58
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
API ввода вопросов - это полезный инструмент для интеграции моделей машинного обучения, таких как ChatGPT, в приложения, требующие обработки...
0
4
68
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

На этот вопрос был дан ответ в виде комментария (см. комментарий mkopriva здесь), но позвольте мне предоставить версию с «подтвержденным ответом».

Сначала небольшое отступление:

done := make(chan interface{})

Я обычно вижу make(chan struct{}) здесь. Поскольку фактическое значение никогда не отправляется, тип канала не имеет большого значения, но отправка пустого значения struct вообще не занимает места, а отправка пустого interface{} занимает место.1

Теперь, что мы собираемся сделать здесь, в заключении2, это:

  • дождитесь (или хотя бы притворитесь, что ждете) ответа какого-нибудь сервера;
  • в случае тайм-аута перестать ждать сервер; и
  • доставить наш идентификатор в канал результатов

или если канал done закрыт (указывая на то, что кто-то другой опередил нас, чтобы сделать все), не беспокойтесь ни о чем из вышеперечисленного.

В качестве сложности мы также будем регистрировать, как долго мы ждали, даже если не получили ответа.

Основная горутина:

  • создает канал done, единственная цель которого — быть closed, чтобы получатели от него немедленно возвращали нулевое значение отсутствия значения в EOF;
  • выделяет некоторое количество (если быть точным, 10) этих рабочих горутин;
  • ждет, пока первый выдаст результат (возможно, отсутствие результата из-за тайм-аута, результат)
  • закрывает канал done, чтобы оставшиеся воркеры прекратили работу; и
  • печатает окончательный результат.

Нас интересует, почему код замыкания написан фрагментом кода:

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

в этом.

Хитрость здесь в том, что select заранее оценивает все свои альтернативы. Таким образом, он оценивает канал done, а также вызывает time.After(), прежде чем начать процесс выбора. Затем select ожидает того, что имеет значение или находится в конце канала и, следовательно, имеет EOF, в зависимости от того, что произойдет раньше.

Если ни одна горутина еще не отправила результат обратно в основную горутину, канал done не будет закрыт. В этот момент все горутины будут блокироваться на канале done. Но все горутины также вызывают time.After.

Код time.After запускает горутину, которая через некоторое время отправляет текущее время на канал. Затем он возвращает этот канал. Следовательно, по крайней мере одна из этих двух операций <- завершится: либо канал done будет закрыт, либо будет закрыт, и мы получим на нем нулевое значение из-за EOF, либо каналу, возвращенному time.After, будет отправлено время. к нему, и мы получим это значение. Независимо от того, какое значение мы на самом деле получаем, мы опускаем значение на пол, но тот факт, что один из двух операторов <- в конечном итоге разблокируется, гарантирует, что эта горутина в конечном итоге сможет продолжить работу.

Событие, которое произойдет первым, будет закрытием канала 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.

По истечении времени ожидания мы должны попытаться отправить наш идентификатор в основную горутину. Однако у нас может не получиться это сделать: какая-нибудь другая рабочая горутина может опередить нас в этой отправке, а основная горутина считывает только одно значение из канала. Чтобы мы не застряли здесь, у нас есть еще один select. Мы попробуем отправить наш идентификатор, но остановимся, если канал done сейчас или закроется. Тогда мы зарегистрируемся и вернемся.


1Я продолжаю думать, что Go должен иметь предварительно объявленный пустой тип структуры просто из соображений удобства и стиля. Мы бы использовали это здесь для нашего канала done. Мы бы использовали это для карт, которые существуют исключительно для того, чтобы действовать как наборы, за исключением того, что они также будут иметь предварительно объявленный тип только для удобства и стиля. Но это совсем другое дело.

2Здесь нет особых причин использовать замыкание. Неэкспортированная простая функция будет работать так же хорошо. Учитывая, что мы используем замыкание, мы могли бы захватить канал done, значение wg *WaitGroup и канал result вместо того, чтобы принимать их в качестве аргументов. Мне непонятно, почему автор решил написать это как замыкание, которое могло бы быть функцией, а затем не беспокоиться ни о каких приятных вещах, которые дает нам замыкание.

Благослови вас и @mkopriva

Chris Ngo 26.12.2020 09:14

Другие вопросы по теме