Я читаю параллелизм в 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:
, результаты не синхронизировались.
default
случай, выбран этот случай. Если нет случая по умолчанию, оператор «выбрать» блокируется до тех пор, пока не будет продолжено хотя бы одно из сообщений».
@mkopriva это круто, спасибо за разъяснение! Я посмотрел на gobyexample.com/non-blocking-channel-operations. Означает ли это, что time.After
фактически не имеет отношения к процессу? Какой смысл иметь два отдельных блока выбора, если мы никогда не используем значение? Второй блок выбора имеет смысл как единственный блок, поскольку он также обрабатывает <-done
Если <-done
слишком долго выполняет свою работу, time.After
можно использовать в качестве альтернативного пути. Например, «лимит» того, как долго select
должен ждать другого cases
. (Я не знаю, как именно он используется в коде вопроса или ссылке в комментарии, так как я только просмотрел код и не понимаю его назначения).
На этот вопрос был дан ответ в виде комментария (см. комментарий mkopriva здесь), но позвольте мне предоставить версию с «подтвержденным ответом».
Сначала небольшое отступление:
done := make(chan interface{})
Я обычно вижу make(chan struct{})
здесь. Поскольку фактическое значение никогда не отправляется, тип канала не имеет большого значения, но отправка пустого значения struct
вообще не занимает места, а отправка пустого interface{}
занимает место.1
Теперь, что мы собираемся сделать здесь, в заключении2, это:
или если канал done
закрыт (указывая на то, что кто-то другой опередил нас, чтобы сделать все), не беспокойтесь ни о чем из вышеперечисленного.
В качестве сложности мы также будем регистрировать, как долго мы ждали, даже если не получили ответа.
Основная горутина:
done
, единственная цель которого — быть close
d, чтобы получатели от него немедленно возвращали нулевое значение отсутствия значения в EOF;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
«Когда мы когда-либо использовали значение, возвращаемое из этого канала». Никогда не используется операция приема, а не получаемое значение. «Как этот канал вообще передается за пределы блока выбора?» Канал не сообщается. Однако получение сообщения приводит к тому, что
select
перестает блокироваться.select
блокируется до тех пор, пока либоdone
не получит сообщение, либоtime.After(simulatedLoadTime)
не получит сообщение. Если вы добавитеdefault
, тоselect
вообще не будет блокироваться.