Почему всегда плохо пропускать вызов функции отмены для дочернего контекста?

описание проблемы

При использовании пакета «context» в Go проверка «lostcancel» команды go vet выдаст предупреждение, если вы не вызовете функцию отмены, возвращаемую context.WithCancel(). например если у вас есть:

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    foo(ctx)
    fmt.Println("done")
}

...тогда Go vet предупредит вас:

функция отмены используется не на всех путях (возможна утечка контекста)

Это также упоминается в документации пакета Context:

Если не удается вызвать CancelFunc, происходит утечка дочернего и его дочерних элементов до тех пор, пока родитель не будет отменен или не сработает таймер. Инструмент go vet проверяет, используются ли функции CancelFunc на всех путях потока управления.

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

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

Мне кажется, это очень естественный вариант использования древовидной структуры родительского/дочернего пакета контекста. Но это дает ту же ошибку go vet, что и упомянутая выше, потому что в пути выполнения кода, представляющем второй вариант (позволить дочернему элементу жить в течение той же продолжительности, что и родительский), функция отмены дочернего контекста не будет вызываться.

Итак, действительно ли это недостаток go vet/рекомендаций сообщества Go по использованию контекста, или это ошибка в моих размышлениях на эту тему?

Пример программы

Вот пример программы, в которой возникает ошибка. Go vet вынесет предупреждение за линию barctx, barcancel := context.WithCancel(ctx). Этот пример явно глупый, потому что я мог бы просто проверить bar на наличие ошибки перед запуском горутины. Но достаточно сказать: в моей реальной программе это было бы невозможно.

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    foo(ctx)
    cancel()

    time.Sleep(10 * time.Millisecond)
    fmt.Println("done")
}

func foo(ctx context.Context) {
    go func() {
        <-ctx.Done()
        fmt.Println("foo done")
    }()

    for i := 0; i < 2; i++ {
        barctx, barcancel := context.WithCancel(ctx)
        err := bar(barctx, i)
        if err != nil {
            fmt.Println("bar error; canceled")
            barcancel()
        }
    }
}

func bar(ctx context.Context, x int) (err error) {
    go func() {
        <-ctx.Done()
        fmt.Printf("bar done; x=%d\n", x)
    }()

    if x > 0 {
        err = fmt.Errorf("bar error")
    }
    return err
}
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
API ввода вопросов - это полезный инструмент для интеграции моделей машинного обучения, таких как ChatGPT, в приложения, требующие обработки...
1
0
83
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В вашем примере go vet было бы правильно вернуть предупреждение. Проблема конкретно заключается в функции bar. Когда вы его создаете, bar создает горутину, которая блокируется до тех пор, пока не будет вызвана barcancel или cancel. До тех пор он будет продолжать существовать. Поэтому крайне важно вызвать функцию отмены, чтобы ресурсы, ожидающие выполнения контекста, могли выйти за пределы области действия и быть удалены сборщиком мусора.

Чтобы было ясно, в вашем примере все отложенные подпрограммы будут отменены, потому что вы отменили ctx, который также отменил каждый экземпляр barctx. Однако вы не должны зависеть от этого, поскольку вы можете не контролировать, будет ли отменен родительский контекст. Итак, ваша функция должна взять на себя ответственность за контексты, которые она создает и использует при вызовах функций.

Чтобы было ясно, вы должны изменить свой примерный код следующим образом:

barctx, barcancel := context.WithCancel(ctx)
err := bar(barctx, i)
if err != nil {
    fmt.Println("bar error; canceled")
}

barcancel()

Но в этом примере я не хочу, чтобы горутина в bar() была отменена, если она не вернет ошибку. Как я уже сказал, я хочу, чтобы он жил так же долго, как родитель. Это правда, что я делаю предположение, что у меня есть контроль над родителем, но это также верно в этом случае. Это может быть правдой во многих случаях. Го ветеринар мог ясно видеть, что это правда. Но я все равно понимаю вашу точку зрения, я думаю, я просто не использую контекст так, как его предполагалось использовать.

tbell5 08.11.2022 03:07

@ tbell5 Я понимаю твою точку зрения. В вашем примере вы пытаетесь создать функцию «установил и забыл», которая отменяется только при возникновении ошибки или отмене родительского контекста?

Woody1193 08.11.2022 05:45

«Когда вы его создаете, bar создает горутину, которая блокируется до тех пор, пока не будет вызвана barcancel. До тех пор она будет продолжать существовать, даже если ваша основная функция завершится». Это не верно.

Volker 08.11.2022 06:17

@ Woody1193 Да, верно. Но теперь я вижу, что не использую дочерний контекст по назначению. Я думаю, что у меня просто плохо спроектированный код, и, вероятно, его следует просто реорганизовать.

tbell5 08.11.2022 16:56

@Volker После дальнейшего изучения я вижу, что вы правы. Спасибо, что просветили меня

Woody1193 09.11.2022 01:52

Другие вопросы по теме