#20.4 golang select Горутины и первая выполненная операция с каналом - case, default
Primary tabs
Forums:
Select - выбор первой завершенной горутины
Оператор select позволяет остановить выполнение текущей горутины, до момента пока для одного из вариантов case не удастся совершить "коммуникационную операцию" (channel communication operation) с каналом, а именно:
- либо записать данные в канал - канал должен быть либо пуст, либо иметь непустой буфер), иначе блокировка, до момента освобождения места в канале
- либо считать данные из канала - тут горутина блокируется, то тех пор пока появятся данные
-- как только один из case-ов оказывается пригоден для выполнения, он выполняется и разблокированная горутина может выполняться дальше.
Рассмотрим пример:
package main
import (
"fmt"
"math/rand/v2"
"time"
)
// для горутины пишушей в канал
func write(channel chan<- string, comment string) {
channel <- comment // пишем в канал
}
func main() {
simpleChannel := make(chan string)
bufferedChannel := make(chan string, 2)
// запустим код три раза
for i := 0; i < 3; i++ {
go write(bufferedChannel, "Буферизированный канал")
// сравниваем случайно число от 0 до 9 с 5ой
if rand.IntN(10) > 5 {
// добавляем маленькую задержку
time.Sleep(1 * time.Microsecond)
}
go write(simpleChannel, "Обычный канал")
// выбираем первый канал,
// в котором окажется значение
select {
case message := <-bufferedChannel:
fmt.Println("Первым стал:", message)
case message := <-simpleChannel:
fmt.Println("Первым стал:", message)
}
}
}
-- здесь в обоих случаях мы не только ждем появления значения в канале, но и сохраняем его в переменную. Чтобы показать, что каналы могут заполняться в разном порядке мы использовали несколько итераций с случайно добавляемой задержкой.
Примечание: Если каналы заполнились одновременно - select выберет один случайный case!
Использование таймера
В качестве одно из case можно использовать вариант с таймаутом - т.е. этот вариант гарантировано выполнится через указанное нами время и разблокирует текущую горутину, даже
если в остальных вариантах в каналах данные не появятся:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func() {
// ждем две секунды чтобы сработал таймаут
time.Sleep(2 * time.Second)
ch <- "результат"
}()
select {
case res := <-ch:
fmt.Println(res)
case <-time.After(1 * time.Second): // сработает через 1 сек.
fmt.Println("Таймаут!")
}
}
-- самый интересной строкой тут является вызов time.After(), который тоже возвращает канал, в котором через указанное нами время появится значение текущей времени (тип Time cо значением на момент заполнения канала).
В варианте case с таймером в примере выше мы не считaваем это значение переменной, а просто ждем заполнения канала, как сигнала к тому, что можно больше не ждать других вариантов casе, если в их каналах данные не появились.
default - Если другие варианты не сработали
Если все вариаты case заблокированы, а именно, в ситуциях блокировки горутины при использовании каналов:
- либо чтением пустого канала
- либо попыткаой записать в уже заполненный канал
-- и при этом вам требуется продолжить выполнение не дождиясь разблокировки, то на помощь приходит вариант select default, который выполнится, если остальные варианты case продложают блокировать горутину, например:
package main
import (
"fmt"
)
func main() {
channel := make(chan int, 1)
// создали канал и ничего в него не пишем
select {
case value := <-channel:
fmt.Println("Получено:", value)
default:
fmt.Println("Канал пуст!")
}
}
Можно добавить разнообразить пример выше, опять же, добавим случайные таймауты для нескольких итераций цикла:
package main
import (
"fmt"
"math/rand/v2"
"time"
)
func main() {
channel := make(chan int, 1)
for i := 0; i < 5; i++ {
go func() {
channel <- 1
}()
// иногда добавляем таймаут
// чтобы горутина успела записать в канал
if rand.IntN(10) > 5 {
time.Sleep(100 * time.Microsecond)
}
select {
case value := <-channel:
fmt.Println("Получено:", value)
default:
fmt.Println("Канал пуст")
}
}
}
-- если запустим, то получим случайные варианты в каждой строке - канал будет то пуст, то успеет заполнится:
Канал пуст Получено: 1 Получено: 1 Канал пуст Канал пуст
Использование select в циклах и выходе из функции
Рассмотрим довольно необычный пример для вычисления чисел Фиббоначи с использованием двух горутин:
package main
import "fmt"
func fibonacci(channel, quit chan int) {
x, y := 0, 1
/* Этот цикл будет блокировать в каждом витке,
пока вторая горутина
1) либо не считает значение
из канала channel тем самым
разрешив туда очередную запись
2) либо не отправит сигнал выхода в канал quit*/
for {
select {
/* тот случай когда в case указана операция записи -
чтобы она прошла, канал должен быть свободен,
иначе это case будет заблокирован */
case channel <- x:
x, y = y, x+y
// а тут ждем сигнала на выход:
// пока в канале quit нет значений
// этот вариант заблокирован
case <-quit:
fmt.Println("Конец!")
return
}
}
}
func main() {
// основной канал
channel := make(chan int)
// канал, для сигнала выхода
quit := make(chan int)
go func() {
// цикл для вывода очередного числа Фиббоначи
/*
Этот цикл 10 раз ожидает получить из канала
очередное число
*/
for i := 0; i < 10; i++ {
fmt.Println(<-channel)
}
/*
И затем горутине, вычислящей числа в канал
выхода отправляется сигнал на заверншение работы
*/
quit <- 0
}()
// запускаем прослушку каналов
fibonacci(channel, quit)
}
-- тут мы использовали в одном из case операцию записи в канал, он без буфера, но после каждой записи значения считываются в побочной горутине и выводятся в терминал
Если запустим этот код, то получим распечатку вида:
0 1 1 2 3 5 8 13 21 34 Конец!
- Log in to post comments
- 107 reads