В каком случае горутина может быть переключена
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Когда горутина может быть переключена
Горутина может быть переключена (вытеснена) в любой момент, когда она находится в состоянии, которое позволяет это сделать планировщику Go. Это происходит благодаря кооперативной многозадачности с элементами вытеснения. Вот основные случаи:
1. Системные вызовы (syscalls)
Когда горутина выполняет системный вызов (например, чтение файла, сетевые операции), она может быть переключена, потому как системный вызов часто блокирует поток. Планировщик может приостановить горутин в ожидании и запустить другую в этом же потоке.
file, err := os.Open("data.txt") // Системный вызов - горутина может быть переключена
2. Операции с каналами
Блокирующие операции с каналами (чтение/запись) явно позволяют переключиться на другую горутин.
ch := make(chan int)
go func() {
ch <- 42 // Если канал не готов к чтению, горутина может быть переключена
}()
val := <-ch // Если канал пуст, горутина читателя может быть переключена
3. Вызовы runtime.Gosched()
Явный вызов runtime.Gosched() уступает текущий поток, позволяя планировщику запустить другие горутины.
runtime.Gosched() // Горутина добровольно уступает выполнение
4. Работа с синхронизацией (mutex, waitgroup)
При использовании мьютексов, sync.WaitGroup или других механизмов синхронизации горутина может быть переключена, если ресурс заблокирован.
var mu sync.Mutex
mu.Lock() // Если мьютекс уже захвачен, горутина может быть переключена
5. Вытеснение (preemption)
Современный планировщик Go (с версии 1.14) использует вытеснение на основе сигналов, чтобы предотвратить "монополизацию" потока одной горутиной. Это происходит:
- При длительных вычислениях без точек вытеснения (например, tight loop без системных вызовов/сетевых операций).
- По сигналам от операционной системы (например, на Linux через
SIGURG).
for i := 0; i < 1000000000; i++ { // Длительный цикл может вызвать вытеснение
// Логика без блокирующих вызовов
}
6. Вызовы функций, которые могут блокировать
Некоторые функции из стандартной библиотеки или runtime могут создавать точки переключения.
Механизм переключения в деталях
Планировщик Go работает на уровне M (машин, потоков ОС), G (горутин) и P (процессоров, контекстов). Переключение происходит когда:
- Горутина блокируется (канал, мьютекс, системный вызов).
- Горутина завершает выполнение.
- Планировщик решает вытеснить горутин по сигналу или для балансировки нагрузки.
- Явный вызов
runtime.Gosched().
Пример демонстрации переключения
Рассмотрим пример с длительным циклом и вытеснением:
package main
import (
"fmt"
"runtime"
"time"
)
func greedyGoroutine() {
for i := 0; i < 1000000000; i++ {
// Плотные вычисления без точек вытеснения
if i % 100000000 == 0 {
fmt.Println("Greedy:", i) // Периодические выводы могут создавать точки
}
}
}
func normalGoroutine(id int) {
for i := 0; i < 5; i++ {
fmt.Println("Normal", id, ":", i)
time.Sleep(100 * time.Millisecond) // Sleep создает точку переключения
}
}
func main() {
runtime.GOMAXPROCS(1) // Ограничим одним потоком для демонстрации
go greedyGoroutine()
for i := 0; i < 3; i++ {
go normalGoroutine(i)
}
time.Sleep(2 * time.Second)
}
В этом примере даже с одним потоком горутина greedyGoroutine будет периодически вытесняться, позволяя normalGoroutine выполнять свою работу благодаря time.Sleep и вытеснению.
Ключевые выводы
- Переключение горутин происходит кооперативно через блокирующие операции и вытеснение для длительных вычислений.
- Планировщик старается минимизировать латентность и максимизировать utilization потоков ОС.
- Нет гарантий порядка переключения — это зависит от планировщика и состояния системы.
Таким образом, горутина может быть переключена в широком спектре ситуаций, обеспечивая эффективную многозадачность даже на одном потоке ОС.