#20.4 golang select Горутины и первая выполненная операция с каналом - case, default

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
Конец!

Key Words for FKN + antitotal forum (CS VSU):