Привет всем, меня зовут Мин.
В период самостоятельного изучения Golang я написал подробные учебные заметки и разместил их в своем личном общедоступном аккаунте WeChat «Время программирования на Go».Что касается языка Go, я тоже новичок, поэтому то, что я написал, должно больше подходить для новых студентов, если вы только изучаете язык Go, вам стоит обратить на него внимание, учиться и расти вместе.
Мой онлайн-блог:golang.iswbm.comМой Github: github.com/iswbm/GolangCodingTime
1. Что такое контекст?
До Go 1.7 контекст был неорганизованным и существовал в пакете golang.org/x/net/context.
Позже команда Golang обнаружила, что контекст оказался весьма полезным, поэтому он был включен в стандартную библиотеку Go 1.7.
Контекст, также называемый контекстом, его интерфейс определяется следующим образом
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Вы можете видеть, что в интерфейсе Context есть 4 метода.
-
Deadline
: первое возвращаемое значениесрок, в этот момент контекст автоматически инициирует действие «Отмена». Второе возвращаемое значение является логическим значением, true указывает, что крайний срок установлен, а false указывает, что крайний срок не установлен.Если крайний срок не установлен, функция отмены должна быть вызвана вручную, чтобы отменить контекст. -
Done
: возвращает канал только для чтения (возвращается только после отмены) типаstruct{}
. Когда канал доступен для чтения, это означает, что родительский контекст инициировал запрос на отмену.По этому сигналу разработчик может выполнить некоторые действия по очистке и выйти из горутины. -
Err
: возвращает причину отмены контекста. -
Value
: возвращает значение, привязанное к контексту, которое представляет собой пару ключ-значение, поэтому соответствующее значение можно получить только с помощью ключа. Это значение обычно является потокобезопасным.
2. Зачем нам нужен контекст?
Когда горутина запущена, мы не можем заставить ее закрыться.
Общие причины закрытия сопрограммы следующие:
- горутина завершает работу и завершает работу
- Основной процесс падает и завершается, а горутина принудительно завершает работу.
- Отправьте сигнал через канал, чтобы направить отключение сопрограммы.
Первый тип, то есть обычное отключение, не входит в рамки сегодняшнего обсуждения.
Второй — аварийное отключение, и код надо оптимизировать.
Третий метод заключается в том, что разработчики могут вручную управлять сопрограммой Пример кода выглядит следующим образом:
func main() {
stop := make(chan bool)
go func() {
for {
select {
case <-stop:
fmt.Println("监控退出,停止了...")
return
default:
fmt.Println("goroutine监控中...")
time.Sleep(2 * time.Second)
}
}
}()
time.Sleep(10 * time.Second)
fmt.Println("可以了,通知监控停止")
stop<- true
//为了检测监控过是否停止,如果没有监控输出,就表示停止了
time.Sleep(5 * time.Second)
}
В примере мы определяемstop
Чан, уведоми его, чтобы он закончил фоновую горутину. Реализация также очень проста: в фоновой горутине используйте select, чтобы оценитьstop
Можно ли получить значение, если можно получить, значит можно выйти и остановиться, если не получить, то будет выполненоdefault
Логика мониторинга в , продолжайте мониторинг, только пока не получитеstop
объявление о.
Выше приведен сценарий с одной горутиной.Что, если есть несколько горутин и несколько горутин открыты под каждой горутиной? существуетБеспощадный блог BlizzardВот что он сказал о том, почему используется Context
Метод chan+select является более элегантным способом завершения горутины, но этот метод также имеет ограничения Что делать, если есть много горутин, которым нужно контролировать завершение? Что, если эти горутины порождают другие горутины? Что, если есть бесконечные горутины слой за слоем? Это очень сложно, даже если мы определим много chan, решить эту проблему сложно, потому что цепочка отношений горутин делает этот сценарий очень сложным.
Я не согласен с тем, что он сказал здесь, потому что я думаю, что даже использование только одного канала может достичь цели контроля (отмены) нескольких горутин. Давайте используем пример, чтобы убедиться в этом.
Принцип этого примера таков: после использования close для закрытия канала, если канал небуферизованный, он изменит исходную блокировку на неблокирующую, то есть доступную для чтения, но значение чтения всегда будет равно нулю, поэтому согласно этой функции можно судить о том, следует ли закрыть горутину, владеющую каналом.
package main
import (
"fmt"
"time"
)
func monitor(ch chan bool, number int) {
for {
select {
case v := <-ch:
// 仅当 ch 通道被 close,或者有数据发过来(无论是true还是false)才会走到这个分支
fmt.Printf("监控器%v,接收到通道值为:%v,监控结束。\n", number,v)
return
default:
fmt.Printf("监控器%v,正在监控中...\n", number)
time.Sleep(2 * time.Second)
}
}
}
func main() {
stopSingal := make(chan bool)
for i :=1 ; i <= 5; i++ {
go monitor(stopSingal, i)
}
time.Sleep( 1 * time.Second)
// 关闭所有 goroutine
close(stopSingal)
// 等待5s,若此时屏幕没有输出 <正在监控中> 就说明所有的goroutine都已经关闭
time.Sleep( 5 * time.Second)
fmt.Println("主程序退出!!")
}
Вывод выглядит следующим образом
监控器4,正在监控中...
监控器1,正在监控中...
监控器2,正在监控中...
监控器3,正在监控中...
监控器5,正在监控中...
监控器2,接收到通道值为:false,监控结束。
监控器3,接收到通道值为:false,监控结束。
监控器5,接收到通道值为:false,监控结束。
监控器1,接收到通道值为:false,监控结束。
监控器4,接收到通道值为:false,监控结束。
主程序退出!!
Приведенный выше пример показывает, что когда мы определяем небуферизованный канал, если мы хотим закрыть все горутины, мы можем использовать close, чтобы закрыть канал, а затем постоянно проверять, закрыт ли канал во всех горутинах (предпосылка заключается в том, что вы должны согласиться что канал, который вы только закрываете, и никакие другие данные не будут отправлены, иначе отправка данных один раз закроет горутину, что не оправдает наших ожиданий, поэтому лучше всего сделать еще один уровень инкапсуляции на этом канале, чтобы сделать ограничение) решить, завершить ли горутину.
Вот видите, я как новичок так и не нашел нужного повода использовать Context.Могу только сказать, что Context очень полезная штука.С его помощью нам удобно решать некоторые проблемы при работе с параллелизмом, но это не невозможно.или отсутствует.
Другими словами, он решает неНе могли бы выпроблема, но решитьлучше использоватьПроблема.
3. Простое использование контекста
Если я не использую описанный выше метод закрытия канала, есть ли другой более элегантный способ добиться этого?
Да, это контекст, о котором пойдет речь в этой статье.
Я переработал приведенный выше пример, используя Context.
package main
import (
"context"
"fmt"
"time"
)
func monitor(ctx context.Context, number int) {
for {
select {
// 其实可以写成 case <- ctx.Done()
// 这里仅是为了让你看到 Done 返回的内容
case v :=<- ctx.Done():
fmt.Printf("监控器%v,接收到通道值为:%v,监控结束。\n", number,v)
return
default:
fmt.Printf("监控器%v,正在监控中...\n", number)
time.Sleep(2 * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
for i :=1 ; i <= 5; i++ {
go monitor(ctx, i)
}
time.Sleep( 1 * time.Second)
// 关闭所有 goroutine
cancel()
// 等待5s,若此时屏幕没有输出 <正在监控中> 就说明所有的goroutine都已经关闭
time.Sleep( 5 * time.Second)
fmt.Println("主程序退出!!")
}
Код ключа здесь всего три строки
Строка 1: определите отменяемый контекст для родительского контекста с помощью context.Background().
ctx, cancel := context.WithCancel(context.Background())
Строка 2: Затем вы можете использовать комбинацию for + select во всех горутинах, чтобы постоянно проверять, читабельна ли ctx.Done().Если она читабельна, это означает, что контекст был отменен, и вы можете очистить горутину и выйти .
case <- ctx.Done():
Строка 3: если вы хотите отменить контекст, просто вызовите метод отмены. Эта отмена является вторым значением, возвращаемым при создании ctx.
cancel()
Вывод текущего результата выглядит следующим образом. Можно обнаружить, что мы достигли того же эффекта, что и закрытый канал.
监控器3,正在监控中...
监控器4,正在监控中...
监控器1,正在监控中...
监控器2,正在监控中...
监控器2,接收到通道值为:{},监控结束。
监控器5,接收到通道值为:{},监控结束。
监控器4,接收到通道值为:{},监控结束。
监控器1,接收到通道值为:{},监控结束。
监控器3,接收到通道值为:{},监控结束。
主程序退出!!
4. Что такое корневой контекст?
Чтобы создать контекст, мы должны указать родительский контекст.Что нам делать, когда мы хотим создать первый контекст?
Не волнуйтесь, Go уже реализовал два для нас.В начале нашего кода эти два встроенных контекста используются в качестве родительского контекста верхнего уровня, а дополнительные дочерние контексты являются производными.
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
Одним из них является фон, который в основном используется в основной функции, инициализации и тестовом коде.Как контекст верхнего уровня древовидной структуры контекста, то есть корневой контекст, его нельзя отменить.
Один из них — TODO.Если мы не знаем, какой контекст использовать, мы можем использовать это, но в практических приложениях это TODO еще не использовалось.
Оба они по сути являются типами структуры emptyCtx, Context, который нельзя отменить, не имеет установленного срока и не несет никакой ценности.
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
}
5. Наследование и происхождение контекста
При определении нашего собственного контекста выше мы используемWithCancel
Сюда.
Кроме него, пакет контекста имеет несколько других функций серии With.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
У этих четырех функций есть общая особенность, то есть первый параметр получает родительский контекст.
Через одно наследование реализуется еще одна функция, например, с помощью функции WithCancel для передачи в корневой контекст создается дочерний контекст, по сравнению с родительским контекстом дочерний контекст имеет еще одну функцию отмены контекста.
Если в это время мы используем указанный выше подконтекст (context01) в качестве родительского контекста и передаем его в функцию WithDeadline в качестве первого параметра, полученный подподконтекст (context02) сравнивается с подконтекстом ( context01) и Другими словами, есть дополнительная функция автоматической отмены контекста после превышения дедлайна.
Далее я приведу примеры этих контекстов, среди которых выше упоминался WithCancel, а ниже примеры приводить не буду.
Пример 1: с крайним сроком
package main
import (
"context"
"fmt"
"time"
)
func monitor(ctx context.Context, number int) {
for {
select {
case <- ctx.Done():
fmt.Printf("监控器%v,监控结束。\n", number)
return
default:
fmt.Printf("监控器%v,正在监控中...\n", number)
time.Sleep(2 * time.Second)
}
}
}
func main() {
ctx01, cancel := context.WithCancel(context.Background())
ctx02, cancel := context.WithDeadline(ctx01, time.Now().Add(1 * time.Second))
defer cancel()
for i :=1 ; i <= 5; i++ {
go monitor(ctx02, i)
}
time.Sleep(5 * time.Second)
if ctx02.Err() != nil {
fmt.Println("监控器取消的原因: ", ctx02.Err())
}
fmt.Println("主程序退出!!")
}
Вывод выглядит следующим образом
监控器5,正在监控中...
监控器1,正在监控中...
监控器2,正在监控中...
监控器3,正在监控中...
监控器4,正在监控中...
监控器3,监控结束。
监控器4,监控结束。
监控器2,监控结束。
监控器1,监控结束。
监控器5,监控结束。
监控器取消的原因: context deadline exceeded
主程序退出!!
Пример 2: с таймаутом
Методы и функции WithTimeout и WithDeadline в основном одинаковы, что означает, что контекст будет автоматически отменен через определенный период времени.
Единственное отличие, которое мы можем увидеть из определения функции
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
Второй параметр, передаваемый WithDeadline, — это тип time.Time, представляющий собой абсолютное время, означающее, в какое время тайм-аут отменяется.
Второй параметр, передаваемый WithTimeout, — это тип time.Duration, представляющий собой относительное время, означающее, сколько времени потребуется для отмены тайм-аута.
package main
import (
"context"
"fmt"
"time"
)
func monitor(ctx context.Context, number int) {
for {
select {
case <- ctx.Done():
fmt.Printf("监控器%v,监控结束。\n", number)
return
default:
fmt.Printf("监控器%v,正在监控中...\n", number)
time.Sleep(2 * time.Second)
}
}
}
func main() {
ctx01, cancel := context.WithCancel(context.Background())
// 相比例子1,仅有这一行改动
ctx02, cancel := context.WithTimeout(ctx01, 1* time.Second)
defer cancel()
for i :=1 ; i <= 5; i++ {
go monitor(ctx02, i)
}
time.Sleep(5 * time.Second)
if ctx02.Err() != nil {
fmt.Println("监控器取消的原因: ", ctx02.Err())
}
fmt.Println("主程序退出!!")
}
Выход такой же, как указано выше
监控器1,正在监控中...
监控器5,正在监控中...
监控器3,正在监控中...
监控器2,正在监控中...
监控器4,正在监控中...
监控器4,监控结束。
监控器2,监控结束。
监控器5,监控结束。
监控器1,监控结束。
监控器3,监控结束。
监控器取消的原因: context deadline exceeded
主程序退出!!
Пример 3: со значением
Через Контекст мы также можем передать некоторые необходимые метаданные, которые будут прикреплены к Контексту для использования.
Метаданные передаются как ключ-значение, ключ должен быть сопоставим, а значение должно быть потокобезопасным.
По-прежнему используя приведенный выше пример, возьмите ctx02 в качестве родительского контекста, а затем создайте ctx03, который может нести значение.Поскольку его родительским контекстом является ctx02, ctx03 также имеет функцию автоматической отмены с течением времени.
package main
import (
"context"
"fmt"
"time"
)
func monitor(ctx context.Context, number int) {
for {
select {
case <- ctx.Done():
fmt.Printf("监控器%v,监控结束。\n", number)
return
default:
// 获取 item 的值
value := ctx.Value("item")
fmt.Printf("监控器%v,正在监控 %v \n", number, value)
time.Sleep(2 * time.Second)
}
}
}
func main() {
ctx01, cancel := context.WithCancel(context.Background())
ctx02, cancel := context.WithTimeout(ctx01, 1* time.Second)
ctx03 := context.WithValue(ctx02, "item", "CPU")
defer cancel()
for i :=1 ; i <= 5; i++ {
go monitor(ctx03, i)
}
time.Sleep(5 * time.Second)
if ctx02.Err() != nil {
fmt.Println("监控器取消的原因: ", ctx02.Err())
}
fmt.Println("主程序退出!!")
}
Вывод выглядит следующим образом
监控器4,正在监控 CPU
监控器5,正在监控 CPU
监控器1,正在监控 CPU
监控器3,正在监控 CPU
监控器2,正在监控 CPU
监控器2,监控结束。
监控器5,监控结束。
监控器3,监控结束。
监控器1,监控结束。
监控器4,监控结束。
监控器取消的原因: context deadline exceeded
主程序退出!!
6. Примечания по использованию контекста
- Обычно первым параметром функции передается Context (нормативная практика), а имя переменной рекомендуется унифицированно называть ctx
- Контекст является потокобезопасным и может безопасно использоваться в нескольких горутинах.
- Когда вы передаете контекст нескольким горутинам для использования, все горутины могут получать сигнал отмены, если операция отмены выполняется один раз.
- Не передавайте переменные, которые могут быть переданы параметрами функции, в значение контекста.
- Когда функции необходимо получить контекст, но вы не знаете, какой контекст передать, вы можете вместо этого использовать context.TODO вместо передачи nil.
- Когда Контекст отменяется, все дочерние Контексты, которые наследуются от Контекста, будут отменены.