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

Как горутина понимает, что контекст остановлен?

2.0 Middle🔥 231 комментариев
#Конкурентность и горутины#Производительность и оптимизация

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

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

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

Механизм отслеживания остановки контекста в горутинах

Горутина в Go определяет, что контекст остановлен, через проверку канала Done(), который возвращается методом контекста. Это реализовано через механизм отмены, основанный на каналах и селекторах, что является идиоматическим подходом в Go для управления жизненным циклом операций.

Ключевые компоненты контекста

Контекст (context.Context) — это интерфейс, который включает метод Done(), возвращающий канал (<-chan struct{}):

type Context interface {
    Done() <-chan struct{}
    Err() error
    Deadline() (deadline time.Time, ok bool)
    Value(key any) any
}
  • Done() — возвращает закрываемый канал при отмене контекста
  • Err() — возвращает ошибку (context.Canceled или context.DeadlineExceeded) после отмены
  • Deadline() — позволяет узнать, установлен ли таймаут

Как горутина отслеживает отмену

Типичный паттерн использования в горутине:

func worker(ctx context.Context, ch chan<- string) {
    for {
        select {
        case <-ctx.Done():
            // Контекст отменен
            fmt.Printf("Контекст отменен: %v\n", ctx.Err())
            return
        case data := <-inputChan:
            // Обработка данных
            process(data)
        case <-time.After(time.Second):
            // Таймаут операции
        }
    }
}

Внутренняя реализация отмены

Рассмотрим на примере context.WithCancel:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

type cancelCtx struct {
    Context
    mu       sync.Mutex
    done     atomic.Value // хранит chan struct{}
    children map[canceler]struct{}
    err      error
}

func (c *cancelCtx) Done() <-chan struct{} {
    d := c.done.Load()
    if d != nil {
        return d.(chan struct{})
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    d = c.done.Load()
    if d == nil {
        d = make(chan struct{})
        c.done.Store(d)
    }
    return d.(chan struct{})
}

При вызове функции отмены (cancel()) происходит:

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // уже отменен
    }
    c.err = err
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
        c.done.Store(closedchan) // предварительно созданный закрытый канал
    } else {
        close(d) // Закрытие канала!
    }
    // Рекурсивная отмена всех дочерних контекстов
    for child := range c.children {
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()
}

Практические аспекты отслеживания

  1. Блокирующее чтение из канала — операция <-ctx.Done() блокирует выполнение, пока канал не будет закрыт
  2. Неблокирующая проверка — можно использовать select с default для опциональной проверки:
select {
case <-ctx.Done():
    // Контекст отменен
    return ctx.Err()
default:
    // Контекст активен, продолжаем работу
}
  1. Комбинирование с другими каналами — контекст идеально сочетается с другими асинхронными операциями:
func fetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}
  1. Распространение отмены — при создании цепочки контекстов отмена родительского автоматически отменяет все дочерние:
parentCtx, cancelParent := context.WithCancel(context.Background())
childCtx, _ := context.WithCancel(parentCtx)

// Отмена родительского контекста
cancelParent()

// Дочерний контекст также будет отменен
<-childCtx.Done() // Не блокируется - канал уже закрыт

Важные нюансы

  • Канал закрывается только один раз — повторные закрытия приведут к панике
  • Нулевой контекстcontext.Background() и context.TODO() никогда не отменяются
  • Производительность — проверка контекста в горячих циклах может создать нагрузку, в таких случаях лучше проверять реже
  • Утечки ресурсов — всегда вызывайте функцию отмены для WithCancel и WithTimeout, даже если операция завершилась успешно:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // Гарантированное освобождение ресурсов

Горутина понимает об остановке контекста через закрытие канала Done(), что активирует соответствующий case в select-блоке или разблокирует ожидающую операцию чтения. Этот механизм обеспечивает эффективную и безопасную координацию между горутинами без необходимости активного опроса.

Как горутина понимает, что контекст остановлен? | PrepBro