Управление параллелизмом через контекст в переводе Go

Go

Автор: Самир Аджмани | Адрес:blog.golang.org/context

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

Перевод второго официального блога в основном посвящен контекстному пакету управления параллелизмом Go.

В целом, я думаюПредыдущийЭто основа и ядро ​​параллелизма в Go. context — это набор простых в использовании библиотек, разработанных для управления горутинами на основе предыдущей главы. Ведь между разными горутинами передается только готовый канал, а объем содержащейся информации действительно слишком мал.

В статье кратко представлены методы, предоставляемые контекстом, и то, как они используются. Затем через поисковый пример вводится использование в реальных сценариях.

В конце статьи поясняется, что помимо официально реализованного контекста, существуют и некоторые сторонние реализации, напримерgithub.com/contextа такжеTomb, но они перестали обновляться после появления официального контекста. На самом деле причина очень проста, ведь официальная вообще мощнее. Раньше управление модулем go тоже было в полном расцвете, но недавно официальный запуск собственного решения, возможно скоро, другие методы будут устранены.

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

Переведенный текст выглядит следующим образом:


В сервисах Go каждый запрос будет обрабатываться независимой горутиной, и каждая горутина обычно запускает новую горутину для выполнения некоторой дополнительной работы, например, для доступа к базе данных или службе RPC. Горутины в одном и том же запросе должны иметь возможность делиться доступом к данным запроса, таким как аутентификация пользователя, токены авторизации и крайние сроки запроса. Если запрос отменяется или происходит тайм-аут, все горутины в области запроса должны немедленно выйти для восстановления ресурсов.

В Google мы разработали контекстный пакет, с помощью которого мы можем легко передавать данные запроса, сигналы отмены и информацию о тайм-ауте между горутинами внутри запроса. Посмотреть деталиcontext.

В этой статье будет специально представлено использование пакета контекста и предоставлен полный вариант использования.

Context

Ядром контекста является тип Context. Определяется следующим образом:

// A Context carries a deadline,cancellation signal,and request-scoped values
// across API. Its methods are safe for simultaneous use by multiple goroutines
// 一个 Context 可以在 API (无论是否是协程间) 之间传递截止日期、取消信号、请求数据。
// Context 中的方法都是协程安全的。
type Context interface {
    // Done returns a channel that is closed when this context is cancelled
    // or times out.
    // Done 方法返回一个 channel,当 context 取消或超时,Done 将关闭。
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed
    // 在 Done 关闭后,Err 可用于表明 context 被取消的原因
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    // 到期则取消 context
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none
    Value(key interface{}) interface{}
}

Введение относительно краткое, подробности см.godoc.

Метод Done возвращает канал, который можно использовать для получения сигналов отмены из контекста. Когда канал закрыт, функция, прослушивающая сигнал Done, немедленно прекратит работу, выполняемую в данный момент, и вернется. Метод Err возвращает переменную ошибки, из которой можно узнать, почему контекст был отменен.pipeline and cancelationВ этой статье дается подробное введение в канал Done.

Причина, по которой у Context нет метода отмены, аналогична причине, по которой канал Done доступен только для чтения, то есть горутина, получающая сигнал отмены, не будет отвечать за отмену выдачи сигнала. В частности, когда родитель запускает дочернюю горутину для выполнения операции, дочерний процесс не может отменить родительский. И наоборот, метод WithCancel (описанный далее) предоставляет способ отменить только что созданный контекст.

Контекст безопасен для параллелизма сопрограмм. Мы можем передать контекст любому количеству горутин, и все горутины могут быть просигнализированы отменой.

Метод Deadline позволяет функции решить, нужно ли ей начинать работу, и если оставшееся время слишком мало, начинать работу не стоит. В коде мы можем установить тайм-аут для операций ввода-вывода через крайний срок.

Метод Value позволяет контексту обмениваться данными в области запроса между горутинами, что должно быть безопасным для параллелизма сопрограмм.

Мертвый контекст

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

Контекст, возвращаемый функцией Background, является корнем любого контекста и не может быть отменен.

// Background returns an empty Context. It is never canceled, has no deadline,
// and has no values. Background is typically used in main, init, and tests,
// and as the top-level Context for incoming requests.
// Background 函数返回空 Context,并且不可以取消,没有最后期限,没有共享数据。Background 仅仅会被用在 main、init 或 tests 函数中。
func Background() Context

WithCancel и WithTimeout разветвят новый экземпляр Context, производный экземпляр отменяется раньше, чем родитель. Экземпляр контекста, связанный с запросом, который будет отменен после завершения обработки запроса. При обнаружении нескольких копий запросов данных WithCancel можно использовать для отмены избыточных запросов. WithTimeout можно использовать для установки времени ожидания при запросе серверных служб.

// WithCancel returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed or cancel is called.
// WithCanal 返回父级 Context 副本,当父级的 Done channel 关闭或调用 cancel,它的 Done channel 也会关闭。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// A CancelFunc cancels a Context.
// CancelFunc 用于取消 Context
type CancelFunc func()

// WithTimeout returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed, cancel is called, or timeout elapses. The new
// Context's Deadline is the sooner of now+timeout and the parent's deadline, if
// any. If the timer is still running, the cancel function releases its
// resources.
// 返回父级 Context 副本和 CancelFunc,三种情况,它的 Done 会关闭,分别是父级 Done 关闭,cancel 被调用,和达到超时时间。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithValue предоставляет способ передачи данных, связанных с запросом, через контекст.

// WithValue returns a copy of parent whose Value method returns val for key.
func WithValue(parent Context, key interface{}, val interface{}) Context

Как использовать контекст? Лучший способ — демонстрация кейса.

Пример: веб-поиск Google

Чтобы продемонстрировать пример, реализуйте службу HTTP, которая обрабатывает такие запросы, как /search?q=golang&timeout=1s . timeout указывает, что если время обработки запроса превышает указанное время, отменяет выполнение.

Код в основном включает в себя 3 пакета, а именно:

  • сервер, вход в основную функцию и обработчик /search;
  • userip, реализует общедоступную функцию, которая получает IP-адрес пользователя из контекста запроса;
  • google, реализующий функцию поиска, отвечает за отправку поисковых запросов в Google;

Начните знакомиться!

Package server

Сервер отвечает за обработку запросов типа /search?q=golang и возврат результатов поиска golang.handleSearch — это фактическая функция обработки.Сначала он инициализирует Context с именем ctx и использует defer для реализации функции для выхода из отмены. Если параметр запроса содержит тайм-аут, создайте контекст с помощью WithTimeout, и контекст будет автоматически отменен по истечении тайм-аута.

func handleSearch(w http.ResponseWriter, req *http.Request) {
    // ctx is the Context for this handler. Calling cancel closes the
    // cxt.Done channel, which is the cancellation signal for requests
    // started by this handler
    var (
        ctx context.Context
        cancel context.Context
    )

    timeout, err := time.ParseDuration(req.FromValue("timeout"))
    if err != nil {
        // the request has a timeout, so create a context that is
        // canceled automatically when the timeout expires.
        ctx, cancel = context.WithTimeout(context.Background(), timeout)
    } else {
        ctx, cancel = context.WithCancel(context.Background())
    }
    defer cancel() // Cancel ctx as soon as handlSearch returns.

Затем функция обработки получит ключевое слово запроса и IP-адрес клиента из запроса.IP-адрес клиента получается путем вызова функции пакета userip. При этом, поскольку для запроса бэкенд-сервиса также требуется IP-адрес клиента, он привязан к ctx.

    // Check the search query
    query := req.FormValue("q")
    if query == "" {
        http.Error(w, "no query", http.StatusBadRequest)
        return
    }

    // Store the user IP in ctx for use by code in other packages.
    userIP, err := userip.FormRequest(req)
    if err != nil {
        http.Error(w, e.Error(), http.StatusBadRequest)
        return
    }

    ctx = userip.NewContext(ctx, userIP)

Вызовите google.Search, передав параметры ctx и query.

    // Run the Google search and print the results
    start := time.Now()
    results, err := google.Search(ctx, query)
    elapsed := time.Since(start)

После успешного поиска обработчик отображает страницу результатов.

    if err := resultsTemplate.Execute(w, struct{
        Results     google.Results
        Timeout, Elapsed time.Duration
    }{
        Results: results,
        Timeout: timeout,
        Elaplsed: elaplsed,
    }); err != nil {
        log.Print(err)
        return
    }

Package userip

Пакет userip предоставляет две функции, отвечающие за экспорт IP-адреса пользователя из запроса и привязку IP-адреса пользователя к контексту. Контекст содержит сопоставление ключ-значение, типы ключа и значения — interface{}, ключ должен поддерживать сравнение на равенство, а значение должно быть безопасным для параллелизма сопрограммы. Пакет userip скрывает детали карты, выполняя преобразование типа значения в контексте, то есть IP-адреса клиента. Чтобы избежать конфликтов ключей, userip определяет ключ неэкспортируемого типа.

// The key type is unexported to prevent collision with context keys defined in
// other package
type key int

// userIPkey is the context key for the user IP address. Its value of zero is
// arbitrary. If this package defined other context keys, they would have
// different integer values.
const userIPKye key = 0

Функция FromRequest отвечает за экспорт IP пользователя из http.Request:

func FromRequest(req *http.Request) (net.IP, error) {
    ip, _, err := net.SplitHostPort(req.RemoteAddr)
    if err != nil {
        return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
    }

Функция NewContext генерирует контекст с userIP:

func NewContext(ctx context.Context, userIP net.IP) context.Context {
    return context.WithValue(ctx, userIPKey, userIP)
}

FromContext отвечает за экспорт userIP из Context:

func FromContext(ctx context.Context) (net.IP. bool) {
    // ctx.Value returns nil if ctx has no value for the key;
    // the net.IP type assertion returns ok=false for nil
    userIP, ok := ctx.Value(userIPKey).(net.IP)
    return userIP, ok
}

Package google

google.Search отвечает за запрос интерфейса веб-поиска Google и анализ данных JSON, возвращаемых интерфейсом. Он получает параметр ctx типа Context, если ctx.Done закрыт, он вернет сразу, даже если запрос выполняется.

Параметры запроса включают ключевое слово запроса и IP-адрес пользователя.

func Search(ctx context.Context, query string) (Results, error) {
    // Prepare the Google Search API request
    req, err := http.NewRequest("GET", "http://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil)
    if err != nil {
        return nil, err
    }
    q := req.URL.Query()
    q.Set("q", query)

    // If ctx is carrying the user IP address, forward it to the server
    // Google APIs use the user IP to distinguish server-initiated requests 
    // from end-users requests
    if userIP, ok := userip.FromContext(ctx); ok {
        q.Set("userip", userIP.String())
    }
    req.URL.RawQuery = q.Encode()

Функция поиска использует вспомогательную функцию httpDo, которая отвечает за выполнение HTTP-запроса. Если ctx.Done закрыт, он будет закрыт, даже если запрос выполняется. Поиск передает функцию закрытия в httpDo для обработки ответа.

    var results Results
    err = httpDo(ctx, req, func(resp *http.Response, err error) error {
        if err != nil {
            return err
        }
        defer resp.Body.Close()

        // Parse the JSON search result.
        // https://developers.google.com/web-search/docs/#fonje
        var data struct {
            ResponseData struct {
                Results []struct {
                    TitleNoFormatting string
                    URL               string
                }
            }
        }
        if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
            return err
        }

        for _, res := range data.ResponseData.Results {
            results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL})
        }

        return nil
    })

    return results, err

Функция httpDo запускает новую горутину, отвечающую за выполнение HTTP-запроса и обработку результатов ответа. Если запрос не был выполнен до выхода горутины, если ctx.Done закрыт, выполнение запроса будет отменено.

func httpDo(ctx context.Context, req *http.Request, f func(*http.Request, error) error) error {
    // Run the HTTP request in a goroutine and pass the response to f.
    c := make(chan error, 1)
    req := req.WithContext(ctx)
    go func() { c <- f(http.DefaultClient.Do(req)) }()
    select {
    case <-ctx.Done():
        <- c
        return ctx.Err
    case  err := <-c:
        return err
    }
}

Настройте код в зависимости от контекста

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

译者注:下面介绍的就是开发说的两个 context 的第三方实现,其中有些内容需要简单了解下它们才能完全看懂。

Например, ГориллаcontextРеализует ассоциативную привязку данных, предоставляя карту ключ-значение в запросе. существуетgorilla.go, который обеспечивает реализацию Context, значение, возвращаемое его методом Value, связано с конкретным HTTP-запросом.

Некоторые другие пакеты предоставляют поддержку отмены, аналогичную контексту. Например,TombСуществует метод Kill, который отменяет сигнализацию, закрывая канал Dying. Tomb также предоставляет методы для ожидания выхода горутин, аналогичные sync.WaitGroup. существуетtomb.go, обеспечивает реализацию, в которой текущий контекст будет отменен при отмене родительского контекста или уничтожении гробницы.

Суммировать

В Google для функций, которые получают или отправляют классы запросов, мы требуем, чтобы контекст был передан в качестве первого параметра. Таким образом, даже код Go от разных команд может работать хорошо. Контекст очень удобен для тайм-аута горутины и контроля отмены, а также для обеспечения безопасной доставки важных данных, таких как учетные данные безопасности.

Платформы служб на основе контекста должны реализовывать контекст, чтобы помочь связать платформу и потребителей, а потребители ожидают получить параметры контекста от платформы. Клиентская библиотека, напротив, получает параметр Context от вызывающего объекта. Создав общий интерфейс для данных запроса и управления отменой, разработчики контекста могут легко совместно использовать свой собственный код и создавать более расширяемые службы.


波罗学的微信公众号