Я изучал Голанг и видел, насколько хорош его параллелизм с применением модели только для канала сопрограмм через инновационную конструкцию goroutines.
Одна вещь, которая сразу вызывает у меня беспокойство, - это использование метода Wait()
, используемого для ожидания завершения нескольких невыполненных подпрограмм, порожденных внутри родительской подпрограммы. Цитировать Golang docs
Ожидание может быть использовано для блокировки, пока все goroutines не закончили
Тот факт, что многие разработчики предписывают Wait()
в качестве предпочтительного способа *1016* для реализации параллелизма, кажется противоположным миссии Голанга, позволяющей разработчикам писать эффективное программное обеспечение, потому что блокировка является неэффективным и действительно асинхронным кодом никогда блоков.
Процесс [или поток], который заблокирован, - это процесс, ожидающий какого-либо события, например, когда ресурс станет доступным или завершится операция ввода-вывода.
Другими словами, заблокированный поток будет тратить циклы ЦП, не делая ничего полезного, просто многократно проверяя, может ли текущая запущенная задача прекратить ожидание и продолжить выполнение.
В действительно асинхронном коде, когда сопрограмма сталкивается с ситуацией, когда она не может продолжаться до тех пор, пока не будет получен результат, она должна передать свое выполнение планировщику вместо блокировки, переключив ее состояние от выполняется до ожидание , поэтому планировщик может начать выполнение следующей подпрограммы в очереди из очереди runnable . Состояние ожидания для сопрограммы должно быть изменено с ожидающего на работоспособное только после достижения необходимого результата.
Таким образом, поскольку Wait()
блокирует до тех пор, пока x число goroutines не вызовет Done()
, подпрограмма, которая вызывает Wait()
, всегда будет оставаться либо в рабочем состоянии, либо в рабочем состоянии, тратя циклы ЦП и полагаясь на планировщик, чтобы выгрузить длительно работающая программа только для того, чтобы изменить ее состояние с работающего на работоспособное, вместо того, чтобы изменить его на ожидание, как и должно быть.
Если все это правда, и я понимаю, как Wait()
работает правильно, то почему люди не используют встроенные каналы Go для задачи ожидания выполнения подпрограмм? Если я правильно понимаю, отправка в буферизованный канал и чтение из любого канала являются асинхронными операциями, а это означает, что их вызов переведет программу в состояние ожидания, так почему же они не являются предпочтительным методом?
В статье, на которую я ссылался, приводится несколько примеров. Вот что автор называет «старой школой»:
package main
import (
"fmt"
"time"
)
func main() {
messages := make(chan int)
go func() {
time.Sleep(time.Second * 3)
messages <- 1
}()
go func() {
time.Sleep(time.Second * 2)
messages <- 2
}()
go func() {
time.Sleep(time.Second * 1)
messages <- 3
}()
for i := 0; i < 3; i++ {
fmt.Println(<-messages)
}
}
и вот предпочтительный, "канонический" способ:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
messages := make(chan int)
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
time.Sleep(time.Second * 3)
messages <- 1
}()
go func() {
defer wg.Done()
time.Sleep(time.Second * 2)
messages <- 2
}()
go func() {
defer wg.Done()
time.Sleep(time.Second * 1)
messages <- 3
}()
wg.Wait()
for i := range messages {
fmt.Println(i)
}
}
Я могу понять, что второе может быть легче для понимания, чем первое, но первое является асинхронным, когда никакие сопрограммы не блокируются, а второе имеет одну сопрограмму, которая блокирует: ту, которая выполняет основную функцию. Здесь является еще одним примером того, что Wait()
является общепринятым подходом.
Почему Wait()
не считается анти-паттерном сообществом Go, если оно создает неэффективный заблокированный поток? Почему в этой ситуации большинство не предпочитают каналы, поскольку их можно использовать для обеспечения асинхронности всего кода и оптимизации потока?