Go ctx.Done() никогда не срабатывает в инструкции select

# #go #backend

Вопрос:

У меня есть следующий код для модуля, который я разрабатываю, и я не уверен, почему provider.Shutdown() функция никогда не вызывается при вызове .Stop()

Основной процесс останавливается, но я не понимаю, почему это не работает?

 package pluto

import (
    "context"
    "fmt"
    "log"
    "sync"
)

type Client struct {
    name string
    providers []Provider
    cancelCtxFunc context.CancelFunc
}

func NewClient(name string) *Client {
    return amp;Client{name: name}
}

func (c *Client) Start(blocking bool) {
    log.Println(fmt.Sprintf("Starting the %s service", c.name))

    ctx, cancel := context.WithCancel(context.Background())
    c.cancelCtxFunc = cancel // assign for later use

    var wg sync.WaitGroup

    for _, p := range c.providers {
        wg.Add(1)

        provider := p
        go func() {
            provider.Setup()

            select {
                case <-ctx.Done():
                    // THIS IS NEVER CALLED?!??!
                    provider.Shutdown()
                    return
                default:
                    provider.Run(ctx)
            }
        }()
    }

    if blocking {
        wg.Wait()
    }
}

func (c *Client) RegisterProvider(p Provider) {
    c.providers = append(c.providers, p)
}

func (c *Client) Stop() {
    log.Println("Attempting to stop service")
    c.cancelCtxFunc()
}

 

Код клиента

 
package main

import (
    "pluto/pkgs/pluto"
    "time"
)

func main() {
    client := pluto.NewClient("test-client")

    testProvider := pluto.NewTestProvider()
    client.RegisterProvider(testProvider)

    client.Start(false)

    time.Sleep(time.Second * 3)
    client.Stop()
}
 

Комментарии:

1. Вполне вероятно, что программа завершится до того, как оператор select сможет запуститься.

2. Не говоря уже о том , что вы почти гарантированно окажетесь в default деле, заблокированном provider.Run , так как же вы могли когда-либо вернуться к этому <-ctx.Done(): делу?

3. @BurakSerdar Я думаю, что ты прав. Я только что понял, что если я проведу время. Спите после вызова функции отмены, затем код работает. Можно ли установить 30-секундный максимальный тайм-аут, но отменить его раньше, если все закончено?

Ответ №1:

Потому что он уже выбрал другой case , прежде чем контекст будет отменен. Вот ваш код с комментариями:

     // Start a new goroutine
    go func() {
        provider.Setup()
        
        // Select the first available case
        select {
            // Is the context cancelled right now?
            case <-ctx.Done():
                // THIS IS NEVER CALLED?!??!
                provider.Shutdown()
                return
            // No? Then call provider.Run()
            default:
                provider.Run(ctx)
                // Run returned, nothing more to do, we're not in a loop, so our goroutine returns
        }
    }()
 

После provider.Run вызова отмена контекста ничего не сделает в показанном коде. provider.Run также получает контекст, поэтому он может обрабатывать отмену так, как считает нужным. Если вы хотите, чтобы в вашей программе также наблюдалась отмена, вы можете включить это в цикл:

     go func() {
        provider.Setup()

        for {
        select {
            case <-ctx.Done():
                // THIS IS NEVER CALLED?!??!
                provider.Shutdown()
                return
            default:
                provider.Run(ctx)
        }
        }
    }()
 

Таким образом, как только provider.Run он вернется, он повторится select снова, и если контекст был отменен, это обращение будет вызвано. Однако, если контекст не был отменен, он вызовет provider.Run снова, что может быть или не быть тем, что вы хотите.

Редактировать:

Более типично, что у вас будет один из нескольких сценариев, в зависимости от того, как provider.Run и provider.Shutdown как это работает, что не было ясно указано в вопросе, поэтому вот ваши варианты:

Shutdown должен вызываться при отмене контекста и Run должен вызываться только один раз:

 go func() {
    provider.Setup()
    go provider.Run(ctx)
    go func() {
        <- ctx.Done()
        provider.Shutdown()
    }()
}
 

Или Run , который уже получает контекст, уже делает то же самое, Shutdown что и при отмене контекста, и поэтому вызов Shutdown при отмене контекста совершенно не нужен:

 go provider.Run(ctx)
 

Комментарии:

1. Спасибо! у тебя есть какие-нибудь идеи, как бы я просто добрался до провайдера. Бежать, чтобы бежать один раз? (провайдер. Запуск будет потенциально заблокирован, например, он может запустить HTTP-сервер внутри)

2. @Calum: есть ли причина, по которой вы не просто работаете <-ctx.Done(); provider.Shutdown() одновременно?

3. Вы могли бы использовать флаг, на самом деле существует множество решений, но, по сути, этот дизайн, скорее всего, неверен с самого начала. В частности, я задаюсь вопросом, нужно ли вам звонить provider.Shutdown : а) если provider.Run уже был вызван и возвращен, и б) если переданный контекст provider.Run был отменен. Я бы подумал, что в обоих случаях ответ «Нет», что означает, что вы можете заменить весь цикл и select просто provider.Run(ctx) .

4. @Calum см. правки — в типичном коде Go здесь есть две общие возможности, одна из которых заключается в том, что Джим, а другая в том, что ни один из этого кода не нужен.

5. Спасибо, Адриан, я ценю твою помощь.