предисловие
Эта статья познакомит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!")
}
В приведенном выше примере определяетсяbuffer
0channel 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
основаны наBackground
;иTODO
Это неопределенно?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
То есть он используется для установления логики разъединения между текущим узлом и узлом-предком.
- если
parent.Done()
возвращениеnil
, указывающее, что на пути над родительским узлом нет отменяемых элементов.context
, не нужно обрабатывать; - если в
context
нашел на цепочкеcancelCtx
введите узел-предок, затем оцените, был ли отменен узел-предок, если он был отменен, отмените текущий узел; в противном случае добавьте текущий узел к узлу-предкуchildren
список. - В противном случае запустите сопрограмму и слушайте
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) }
}
- Если родительский узел
parent
Есть время истечения и время истечения раньше указанного времениd
, то новый дочерний узелcontext
Нет необходимости устанавливать время истечения, используйтеWithCancel
создать отменяемыйcontext
Ты сможешь; - В противном случае используйте
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
, а затем начать чтение сетевых запросов из текущего соединения, и всякий раз, когда запрос будет прочитан, он будетcancelCtx
In, чтобы передать сигнал отмены. После потери соединения может быть отправлен сигнал отмены, чтобы отменить все текущие сетевые запросы.
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
), поэтому его можно безопасно передавать в нескольких сопрограммах.