← Назад к вопросам

Как горутина переходит в блокировку?

2.7 Senior🔥 152 комментариев
#Конкурентность и горутины#Основы Go

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Механизм блокировки горутин в Go

Горутина — это легковесный поток выполнения в Go, управляемый планировщиком runtime. Блокировка горутины происходит, когда она не может продолжать выполнение до наступления определенного события. Этот процесс фундаментален для понимания конкурентности в Go.

Основные причины блокировки горутин

1. Синхронизация через каналы

Наиболее частая причина — операции с каналами, которые не могут быть немедленно выполнены.

ch := make(chan int)

// Горутина заблокируется здесь до получения данных
go func() {
    data := <-ch  // Блокировка на чтении из пустого канала
    fmt.Println(data)
}()

time.Sleep(time.Second)
ch <- 42  // Разблокировка горутины

2. Мьютексы и примитивы синхронизации

var mu sync.Mutex

go func() {
    mu.Lock()      // Блокировка, если мьютекс уже захвачен
    defer mu.Unlock()
    // Критическая секция
}()

3. Системные вызовы и I/O операции

Горутина блокируется при выполнении системных вызовов (файловые операции, сетевые запросы), пока ядро ОС не завершит операцию.

4. Вызовы runtime.Gosched() и sleep

go func() {
    time.Sleep(2 * time.Second)  // Явная блокировка на время
    runtime.Gosched()             // Добровольная уступка процессорного времени
}()

Как планировщик управляет блокировками

Этапы перехода в блокировку

  1. Обнаружение блокирующей операции Планировщик идентифицирует, что горутина пытается выполнить операцию, которая не может быть завершена немедленно.

  2. Изменение состояния горутины Состояние меняется с Grunning на Gwaiting. Каждая причина блокировки имеет свой wait reason, который можно увидеть в трассировках:

// Пример кода, демонстрирующего разные состояния
package main

import (
    "runtime"
    "time"
)

func main() {
    ch := make(chan int)
    
    go func() {
        <-ch  // waitReasonChanReceive
    }()
    
    go func() {
        var mu sync.Mutex
        mu.Lock()  // waitReasonSyncMutexLock
    }()
    
    time.Sleep(time.Millisecond)
}
  1. Связывание с объектом ожидания Горутина связывается с объектом, который вызвал блокировку (каналом, мьютексом, сокетом).

  2. Вытеснение из потока (M) Заблокированная горутина удаляется из потока выполнения (M), освобождая его для других горутин.

  3. Переход в очередь ожидания Горутина помещается в соответствующую очередь ожидания:

    • Для каналов — в очередь отправки или получения
    • Для мьютексов — в очередь ожидающих горутин
    • Для network poller — в системную очередь ввода-вывода

Сценарий разблокировки

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan string, 1)
    
    // Горутина-отправитель
    go func() {
        fmt.Println("Горутина 1: Попытка отправить...")
        ch <- "данные"  // Не блокируется (буферизованный канал)
        fmt.Println("Горутина 1: Данные отправлены")
    }()
    
    // Горутина-получатель
    go func() {
        time.Sleep(100 * time.Millisecond)
        fmt.Println("Горутина 2: Попытка получить...")
        msg := <-ch  // Получает без блокировки
        fmt.Println("Горутина 2: Получено:", msg)
    }()
    
    time.Sleep(time.Second)
}

Важные особенности блокировки

Невытесняющая модель в точках блокировки

Планировщик Go не вытесняет горутины во время обычного выполнения — только в определенных точках:

  • При вызове функций (стековые проверки)
  • При операциях с каналами
  • При операциях с примитивами синхронизации
  • При системных вызовах

Network poller интеграция

Для сетевых операций Go использует асинхронный ввод-вывод через network poller (на основе epoll/kqueue/IOCP). Заблокированные на I/O горутины регистрируются в поллере и возобновляются при готовности данных.

Работа планировщика во время блокировок

// Пока одна горутина заблокирована, планировщик выполняет другие
func example() {
    done := make(chan bool)
    
    for i := 0; i < 3; i++ {
        go func(id int) {
            time.Sleep(time.Duration(id) * 100 * time.Millisecond)
            fmt.Printf("Горутина %d выполнена\n", id)
            if id == 2 {
                done <- true
            }
        }(i)
    }
    
    <-done
}

Практические последствия

  1. Эффективное использование потоков ОС Тысячи горутин могут блокироваться и возобновляться, используя небольшое количество потоков ОС.

  2. Автоматическое распараллеливание Планировщик автоматически распределяет незаблокированные горутины по доступным ядрам CPU.

  3. Отсутствие busy waiting Заблокированные горутины не потребляют CPU, что делает Go эффективным для I/O-bound задач.

  4. Риск взаимоблокировок (deadlock)

    // Классический deadlock
    func main() {
        ch := make(chan int)
        <-ch  // Блокировка навсегда (deadlock)
    }
    

Блокировка горутин — это не недостаток, а фундаментальный механизм, позволяющий Go эффективно управлять тысячами одновременных операций, минимизируя потребление ресурсов и максимизируя утилизацию CPU во время ожидания I/O операций. Понимание этого процесса критически важно для написания эффективных и корректных конкурентных программ на Go.