Глубокое понимание контекста Голанга

Go
Глубокое понимание контекста Голанга

предисловие

Эта статья познакомитGolangШаблон программирования обычно используется в параллельном программировании:context. Эта статья начнется с того, почемуcontextиди знакомьсяcontextпринцип реализации, и понять, как использоватьcontext.

Зачем нужен контекст

В параллельной программе часто необходимо выполнять вытесняющие операции или прерывать последующие операции из-за тайм-аутов, операций отмены или некоторых нештатных ситуаций. Знаком сchannelдрузья должны были видеть использованиеdone channelдля решения таких вопросов. Например следующий пример:

func main() {
    messages := make(chan int, 10)
    done := make(chan bool)

    defer close(messages)
    // consumer
    go func() {
        ticker := time.NewTicker(1 * time.Second)
        for _ = range ticker.C {
            select {
            case <-done:
                fmt.Println("child process interrupt...")
                return
            default:
                fmt.Printf("send message: %d\n", <-messages)
            }
        }
    }()

    // producer
    for i := 0; i < 10; i++ {
        messages <- i
    }
    time.Sleep(5 * time.Second)
    close(done)
    time.Sleep(1 * time.Second)
    fmt.Println("main process exit!")
}

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

Если бы мы могли прикрепить к простому уведомлению дополнительную информацию, чтобы контролировать отмену: почему оно было отменено, или был ли крайний срок, который должен был завершиться, или если бы было несколько вариантов отмены, нам нужно было бы использовать дополнительную информацию, чтобы решить, какой из них чтобы выбрать вариант «Отмена».

Рассмотрим следующую ситуацию: если в основной сопрограмме есть несколько задач 1, 2, ...m, то основная сопрограмма имеет контроль времени ожидания для этих задач, а задача 1 имеет несколько подзадач 1, 2, ...n, задача 1. Эти подзадачи также имеют собственный контроль времени ожидания, поэтому эти подзадачи должны воспринимать не только сигнал отмены основной сопрограммы, но и сигнал отмены задачи 1.

Если вы все еще используетеdone channelИспользование, нам нужно определить дваdone channel, подзадачи должны контролировать обаdone channel. Хм, вроде все в порядке. Но если иерархия глубже, если у этих подзадач есть подзадачи, то используйтеdone channelстанет очень громоздким и запутанным.

Нам нужно элегантное решение для реализации такого механизма:

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

В настоящее времяcontextЭто пригодилось. Давайте взглянемcontextпринципы проектирования и реализации конструкции.

каков контекст

контекстный интерфейс

Первый взглядContextСтруктура интерфейса выглядит очень просто.

type Context interface {

    Deadline() (deadline time.Time, ok bool)

    Done() <-chan struct{}

    Err() error

    Value(key interface{}) interface{}
}

ContextИнтерфейс содержит четыре метода:

  • DeadlineВозвращает текущую привязкуcontextКрайний срок, за который задача была отменена, если срок не установлен, она вернетсяok == false.
  • Doneпри привязке токаcontextКогда задача будет отменена, она вернет закрытоеchannel; если текущийcontextне будет отменено, вернетсяnil.
  • ErrеслиDoneвозвращениеchannelне закрыто, вернетсяnil;еслиDoneвозвращениеchannelБыла закрыта, вернет ненулевое значение, указывающее причину завершения задачи. еслиcontextбыл отменен,ErrвернусьCanceled;еслиcontextтайм-аут,ErrвернусьDeadlineExceeded.
  • ValueвозвращениеcontextТекущая пара ключа в хранимойkeyсоответствующее значение, если нет соответствующегоkey, затем вернутьсяnil.

можно увидетьDoneметод возвращенchannelОн используется для передачи сигнала окончания для вытеснения и прерывания текущей задачи;DeadlineСпособ указывает на ток после периода времениgoroutineбудет отменено; иErrспособ объяснитьgoroutineпричина отмены; иValueиспользуется для получения дополнительной информации, относящейся к текущему дереву задач. иcontextКак хранится пара «ключ-значение» с дополнительной информацией? На самом деле вы можете представить себе дерево.Каждый узел дерева может содержать набор пар ключ-значение.Если текущий узел не может его найтиkeyСоответствующее значение будет найдено в родительском узле вплоть до корневого узла, который будет обсуждаться позже.

посмотри сноваcontextДругие ключевые элементы пакета.

emptyCtx

emptyCtxЯвляетсяintпеременная типа, но реализуетcontextИнтерфейс.emptyCtxНет тайм-аута, его нельзя отменить и нельзя хранить какую-либо дополнительную информацию, поэтомуemptyCtxиспользуется в качествеcontextКорневой узел дерева.

// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
    return
}

func (*emptyCtx) Done() <-chan struct{} {
    return nil
}

func (*emptyCtx) Err() error {
    return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
    return nil
}

func (e *emptyCtx) String() string {
    switch e {
    case background:
        return "context.Background"
    case todo:
        return "context.TODO"
    }
    return "unknown empty Context"
}

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

func Background() Context {
    return background
}

func TODO() Context {
    return todo
}

Но мы обычно не используем напрямуюemptyCtxвместо этого используяemptyCtxДве переменные, созданные соответственно, могут быть вызваны призваниемBackgroundиTODOспособ получить, но эти дваcontextРеализация такая же. ТакBackgroundиTODOметод полученcontextкакие отличия есть? Взгляните на официальное объяснение:

// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.

// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).

BackgroundиTODOПросто используется в разных сценариях:
BackgroundОбычно используется в основных функциях, инициализации и тестировании в качествеcontext, то есть обычно мы создаемcontextоснованы наBackgroundTODOЭто неопределенно?contextбудет использоваться, когда.

Основы двух разных функций описаны ниже.contextтип:valueCtxиcancelCtx.

valueCtx

структура valueCtx
type valueCtx struct {
    Context
    key, val interface{}
}

func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

valueCtxиспользоватьContextПеременная типа для представления родительского узлаcontext, поэтому токcontextунаследовано от родителейcontextвся информация;valueCtxТип также содержит набор пар ключ-значение, что означает, что этоcontextМожет нести дополнительную информацию.valueCtxДостигнутоValueМетодыcontextПонятьkeyсоответствующее значение, если текущийcontextнет необходимостиkey, последуетcontextГлядя на цепочкуkeyСоответствует значению до корневого узла.

WithValue

WithValueИспользовал кcontextДобавьте пары ключ-значение:

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflect.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

Добавление пар ключ-значение здесь отсутствует в оригиналеcontextДобавьте прямо в структуру, но используйте этоcontextВ качестве родительского узла воссоздайте новыйvalueCtxдочерний узел, добавьте пару ключ-значение к дочернему узлу, таким образом сформировавcontextцепь. ПолучатьvalueПроцесс в этомcontextПоиск с хвоста по цепочке:

cancelCtx

структура cancelCtx
type cancelCtx struct {
    Context

    mu       sync.Mutex            // protects following fields
    done     chan struct{}         // created lazily, closed by first cancel call
    children map[canceler]struct{} // set to nil by the first cancel call
    err      error                 // set to non-nil by the first cancel call
}

type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

иvalueCtxпохожий,cancelCtxесть такжеcontextпеременная как родительский узел; переменнаяdoneОн представляет собойchannel, который используется для указания того, что сигнал закрытия пропускается;childrenозначаетmap, в котором хранится текущийcontextдочерние узлы под узлом;errИспользуется для хранения информации об ошибке с указанием причины завершения задачи.

посмотри сноваcancelCtxРеализованный метод:

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

func (c *cancelCtx) Err() error {
    c.mu.Lock()
    err := c.err
    c.mu.Unlock()
    return err
}

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    // 设置取消原因
    c.err = err
    设置一个关闭的channel或者将done channel关闭,用以发送关闭信号
    if c.done == nil {
        c.done = closedchan
    } else {
        close(c.done)
    }
    // 将子节点context依次取消
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()

    if removeFromParent {
        // 将当前context节点从父节点上移除
        removeChild(c.Context, c)
    }
}

Его можно найтиcancelCtxТиповые переменные на самом делеcancelerтипа потому чтоcancelCtxДостигнутоcancelerинтерфейс.
DoneМетоды иErrНет необходимости говорить о методе.cancelCtxТипcontextвызовcancelметод, причина отмены будет установлена, иdone channelотключитьchannelили закрытьchannel, то дочерние узлыcontextОтменить по очереди, удалив при необходимости текущий узел из родительского узла.

WithCancel

WithCancelфункция для создания отменяемогоcontext,СейчасcancelCtxТипcontext.WithCancelвернутьcontextиCancelFunc,перечислитьCancelFuncдля запускаcancelработать. Посмотрите прямо на исходный код:

type CancelFunc func()

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

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
    // 将parent作为父节点context生成一个新的子节点
    return cancelCtx{Context: parent}
}

func propagateCancel(parent Context, child canceler) {
    if parent.Done() == nil {
        // parent.Done()返回nil表明父节点以上的路径上没有可取消的context
        return // parent is never canceled
    }
    // 获取最近的类型为cancelCtx的祖先节点
    if p, ok := parentCancelCtx(parent); ok {
        p.mu.Lock()
        if p.err != nil {
            // parent has already been canceled
            child.cancel(false, p.err)
        } else {
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
            // 将当前子节点加入最近cancelCtx祖先节点的children中
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
    for {
        switch c := parent.(type) {
        case *cancelCtx:
            return c, true
        case *timerCtx:
            return &c.cancelCtx, true
        case *valueCtx:
            parent = c.Context
        default:
            return nil, false
        }
    }
}

Прежде чем говоритьcancelCtxПри отмене все потомкиcancelCtxотменен,propagateCancelТо есть он используется для установления логики разъединения между текущим узлом и узлом-предком.

  1. еслиparent.Done()возвращениеnil, указывающее, что на пути над родительским узлом нет отменяемых элементов.context, не нужно обрабатывать;
  2. если вcontextнашел на цепочкеcancelCtxвведите узел-предок, затем оцените, был ли отменен узел-предок, если он был отменен, отмените текущий узел; в противном случае добавьте текущий узел к узлу-предкуchildrenсписок.
  3. В противном случае запустите сопрограмму и слушайтеparent.Done()иchild.Done(),однаждыparent.Done()возвращениеchannelБлизко, т.е.contextУзел предка в цепочкеcontextотменяется, текущийcontextтакже отменен.

Здесь может возникнуть вопрос, почему это узел-предок, а не родительский узел? Это потому, что текущийcontextЦепочка может выглядеть так:

ТекущийcancelCtxродительский узелcontextне подлежащий отменеcontext, невозможно записатьchildren.

timerCtx

timerCtxоснован наcancelCtxизcontextТипа, это видно буквально, это своего рода временная отменаcontext.

type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
    return c.deadline, true
}

func (c *timerCtx) cancel(removeFromParent bool, err error) {
    将内部的cancelCtx取消
    c.cancelCtx.cancel(false, err)
    if removeFromParent {
        // Remove this timerCtx from its parent cancelCtx's children.
        removeChild(c.cancelCtx.Context, c)
    }
    c.mu.Lock()
    if c.timer != nil {
        取消计时器
        c.timer.Stop()
        c.timer = nil
    }
    c.mu.Unlock()
}

timerCtxвнутреннее использованиеcancelCtxРеализовать отмену, дополнительно использовать таймерtimerи срок годностиdeadlineРеализуйте функцию отмены по времени.timerCtxвызовcancelметод, сначала преобразует внутреннийcancelCtxотменить, удалить себя изcancelCtxУдаление узла-предка, а затем отмена таймера.

WithDeadline

WithDeadlineвозвращаетparentОтменяемыйcontext, и срок его действияdeadlineне позднее установленного времениd.

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        // The current deadline is already sooner than the new one.
        return WithCancel(parent)
    }
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  d,
    }
    // 建立新建context与可取消context祖先节点的取消关联关系
    propagateCancel(parent, c)
    dur := time.Until(d)
    if dur <= 0 {
        c.cancel(true, DeadlineExceeded) // deadline has already passed
        return c, func() { c.cancel(false, Canceled) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, func() { c.cancel(true, Canceled) }
}
  1. Если родительский узелparentЕсть время истечения и время истечения раньше указанного времениd, то новый дочерний узелcontextНет необходимости устанавливать время истечения, используйтеWithCancelсоздать отменяемыйcontextТы сможешь;
  2. В противном случае используйтеparentи срок годностиdСоздать временную отменуtimerCtx, и создайте новыйcontextс возможностью отменыcontextОтмена отношения ассоциации узла-предка, а затем определение текущего времени по времени истечения срока действияdпродолжительностьdur:
  • еслиdurМеньше 0, то есть текущий период прошел, значит вновь построенныйtimerCtxПричиныDeadlineExceeded;
  • В противном случае новыйtimerCtxУстановите таймер, по истечении времени истечения текущийtimerCtx.
WithTimeout

иWithDeadlineпохожий,WithTimeoutТакже создайте отмену по времениcontext,ТолькоWithDeadlineдолжен получить момент истечения срока действия, иWithTimeoutПолучить время истечения срока действия относительно текущего времениtimeout:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

Использование контекста

первое использованиеcontextРеализовать начало статьиdone channelПример, демонстрирующий, как более элегантно реализовать синхронизацию сигналов отмены между сопрограммами:

func main() {
    messages := make(chan int, 10)

    // producer
    for i := 0; i < 10; i++ {
        messages <- i
    }

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

    // consumer
    go func(ctx context.Context) {
        ticker := time.NewTicker(1 * time.Second)
        for _ = range ticker.C {
            select {
            case <-ctx.Done():
                fmt.Println("child process interrupt...")
                return
            default:
                fmt.Printf("send message: %d\n", <-messages)
            }
        }
    }(ctx)

    defer close(messages)
    defer cancel()

    select {
    case <-ctx.Done():
        time.Sleep(1 * time.Second)
        fmt.Println("main process exit!")
    }
}

В этом случае, если дочерняя резьба дочернего монитора входящего монитораctx,однаждыctx.Done()вернуть пустымchannel, дочерний поток может отменить выполнение задачи. Но этот пример еще не показываетcontextМощное преимущество передачи информации об отмене.

читатьnet/httpДрузья исходного кода пакета могут заметить, что в реализацииhttp serverиспользуется, когдаcontext, и кратко проанализируйте его ниже.

1. ПервыйServerКогда служба запущена,valueCtx, хранитсяserverсоответствующую информацию, каждый раз, когда устанавливается соединение, будет запущена сопрограмма, которая будет нести этуvalueCtx.

func (srv *Server) Serve(l net.Listener) error {

    ...

    var tempDelay time.Duration     // how long to sleep on accept failure
    baseCtx := context.Background() // base is always background, per Issue 16220
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, e := l.Accept()

        ...

        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(ctx)
    }
}

2. После установления соединения оно будет основываться на входящемcontextСоздаватьvalueCtxИспользуется для хранения информации о локальном адресе, а затем создается новый на основе этогоcancelCtx, а затем начать чтение сетевых запросов из текущего соединения, и всякий раз, когда запрос будет прочитан, он будетcancelCtxIn, чтобы передать сигнал отмены. После потери соединения может быть отправлен сигнал отмены, чтобы отменить все текущие сетевые запросы.

func (c *conn) serve(ctx context.Context) {
    c.remoteAddr = c.rwc.RemoteAddr().String()
    ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
    ...

    ctx, cancelCtx := context.WithCancel(ctx)
    c.cancelCtx = cancelCtx
    defer cancelCtx()

    ...

    for {
        w, err := c.readRequest(ctx)

        ...

        serverHandler{c.server}.ServeHTTP(w, w.req)

        ...
    }
}

3. После прочтения запроса он будет основан на входящемcontextСоздать новыйcancelCtxи установите текущий объект запросаreq, созданный одновременноresponseв объектеcancelCtxсохранить текущийcontextМетод отмены.

func (c *conn) readRequest(ctx context.Context) (w *response, err error) {

    ...

    req, err := readRequest(c.bufr, keepHostHeader)

    ...

    ctx, cancelCtx := context.WithCancel(ctx)
    req.ctx = ctx

    ...

    w = &response{
        conn:          c,
        cancelCtx:     cancelCtx,
        req:           req,
        reqBody:       req.Body,
        handlerHeader: make(Header),
        contentLength: -1,
        closeNotifyCh: make(chan bool, 1),

        // We populate these ahead of time so we're not
        // reading from req.Header after their Handler starts
        // and maybe mutates it (Issue 14940)
        wants10KeepAlive: req.wantsHttp10KeepAlive(),
        wantsClose:       req.wantsClose(),
    }

    ...
    return w, nil
}

Основная цель этой обработки заключается в следующем:

  • По истечении времени ожидания текущий запрос может быть прерван;

  • обработка сборкиresponseЕсли во время процесса возникает ошибка, вы можете напрямую вызватьresponseобъектcancelCtxМетод завершает текущий запрос;

  • обработка сборкиresponseПосле завершения позвонитеresponseобъектcancelCtxМетод завершает текущий запрос.

На протяженииserverВ потоке обработкиcontextцепь проходит черезServer,Connection,Request, который не только делится восходящей информацией с нижестоящими задачами, но также понимает, что вышестоящая задача может посылать сигналы отмены для отмены всех нижестоящих задач, а нижестоящие задачи могут отменять себя, не затрагивая вышестоящие задачи.

Суммировать

contextОн в основном используется для сигналов отмены синхронизации между родительскими и дочерними задачами, что по сути является методом планирования сопрограмм. Также используяcontextСтоит отметить две вещи: восходящая задача использует толькоcontextУведомлять нижестоящую задачу о том, что она больше не нужна, но не будет напрямую вмешиваться и прерывать выполнение нижестоящей задачи, а нижестоящая задача сама решает последующие операции обработки, то естьcontextОперация отмены не является навязчивой;contextЭто потокобезопасно, потому чтоcontextсам по себе неизменен(immutable), поэтому его можно безопасно передавать в нескольких сопрограммах.

использованная литература

1,Package context

2,Go Concurrency Patterns: Context

3.Understanding the context package in golang