Давайте поговорим о контексте контекста Go.

Go
Давайте поговорим о контексте контекста Go.

Context

предисловие

Ранее в разделе «Давайте поговорим о http frameworkhttprouter", было упомянуто понятие контекста. Предыдущая демонстрация использовалась для перечисления значений и настроек глобальных переменных в веб-фреймворке.Java Spring ФреймворкизApplicationContext.
На этот раз поговорим о контексте стандартной библиотеки в Go и разберем общие сценарии концепции контекста в Go.

введение проблемы

Прежде чем перечислять использование контекста, давайте рассмотрим простой пример:

Утечка сопрограммы

func main()  {
    //打印已有协程数量
    fmt.Println("Start with goroutines num:", runtime.NumGoroutine())
    //新起子协程
    go Spawn()
    time.Sleep(time.Second)
    fmt.Println("Before finished, goroutines num:", runtime.NumGoroutine())
    fmt.Println("Main routines exit!")
}

func Spawn()  {
    count := 1
    for {
    	time.Sleep(100 * time.Millisecond)
    	count++
    }
}

вывод:

Start with goroutines num: 1
Before finished, goroutines num: 2
Main routines exit!

Мы создаем подпрограмму в основной сопрограмме, используяruntime.NumGoroutine()Выведите текущее количество сопрограмм, вы можете узнать это вглавная сопрограммаВ момент перед выходом после пробуждения в программе еще есть две сопрограммы.Может быть, мы привыкли к этому явлению.Когда основная сопрограмма убивается, выход также убивает под-сопрограмму.Такого рода пусть под- coroutine Практика «самостоятельного следования» на самом деле не очень элегантна.

Решение

Выход уведомления трубы

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

func main()  {
    defer fmt.Println("Main routines exit!")
    ExitBySignal()
    fmt.Println("Start with goroutines num:", runtime.NumGoroutine())
    //主动通知协程退出
    sig <- true
    fmt.Println("Before finished, goroutines num:", runtime.NumGoroutine())
}

//利用管道通知协程退出
func ListenWithSignal()  {
    count := 1
    for {
    	select {
    	//监听通知
    	case <-sig:
    		return
    	default:
    		//正常执行
    		time.Sleep(100 * time.Millisecond)
    		count++
    	}
    }
}

вывод:

Start with goroutines num: 2
Before finished, goroutines num: 1
Main routines exit!

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

управление контекстом

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

официальная концепция

Перед этим давайте рассмотрим концепции стандартной библиотеки:

A Context carries a deadline, a cancellation signal, and other values across API boundaries.

Context's methods may be called by multiple goroutines simultaneously.
Контексты несут крайние сроки, отменяют сигналы и обеспечивают чтение и запись значений между API.

type Context interface {
    // 返回该上下文的截止时间,如果没有设置截至时间,第二个值返回false
    Deadline() (deadline time.Time, ok bool)
    
    // 返回一个管道,上下文结束(cancel)时该方法会执行,经常结合select块监听
    Done() <-chan struct{}
    
    // 当Done()执行时,Err()会返回一个error解释退出原因
    Err() error
    
    // 上下文值存储字典
    Value(key interface{}) interface{}
}

Среди наиболее часто используемыхcontext.Withcancel()функция, которая возвращает оберткуcancel()Подконтекст функции, когда мы думаем, что сопрограмма должна завершиться, вызывает ее возвращаемое значениеcancel()функция, подконтекст закроет внутреннюю инкапсулированную трубуDone()чтобы уведомить соответствующую сопрограмму.

// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    //返回新的cancelCtx子上下文
    c := newCancelCtx(parent)
    //将原上下文
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

Давайте продолжим представлять демонстрацию утечки сопрограммы и посмотрим на конкретный пример использования:
Пример использования:

//阻塞两个子协程,预期只有一个协程会正常退出
func LeakSomeRoutine() int {
    ch := make(chan int)
    //起3个协程抢着输入到ch
    go func() {
    	ch <- 1
    }()
    
    go func() {
    	ch <- 2
    }()
    
    go func() {
    	ch <- 3
    }()
    //一有输入立刻返回
    return <-ch
}

func main() {
    //每一层循环泄漏两个协程
    for i := 0; i < 4; i++ {
    	LeakSomeRoutine()
    	fmt.Printf("#Goroutines in roop end: %d.\n", runtime.NumGoroutine())
    }
}

Вывод программы:

#Goroutines in roop end: 3.
#Goroutines in roop end: 5.
#Goroutines in roop end: 7.
#Goroutines in roop end: 9.

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

Далее мы вводим понятие контекста для управления подпрограммами:

func FixLeakingByContex() {
    //创建上下文用于管理子协程
    ctx, cancel := context.WithCancel(context.Background())
    
    //结束前清理未结束协程
    defer cancel()
    
    ch := make(chan int)
    go CancelByContext(ctx, ch)
    go CancelByContext(ctx, ch)
    go CancelByContext(ctx, ch)
    
    // 随机触发某个子协程退出
    ch <- 1
}

func CancelByContext(ctx context.Context, ch chan (int)) int {
    select {
    case <-ctx.Done():
    	//fmt.Println("cancel by ctx.")
    	return 0
    case n := <-ch :
    	return n
    }
}

func main() {
    //每一层循环泄漏两个协程
    for i := 0; i < 4; i++ {
    	FixLeakingByContex()
    	//给它点时间 异步清理协程
    	time.Sleep(100)
    	fmt.Printf("#Goroutines in roop end: %d.\n", runtime.NumGoroutine())
    }
}

Анализ программы:
можно увидетьCancelByContextФункция опрашивает конвейер, и у программы есть только две ветки для возврата

  • уведомление об окончании контекста
  • Получить значение, переданное конвейером

мы вFixLeakingByContex()откладывать до конца функцииcancel()функция, поэтому перед выходом из программы будет вызвана соответствующая подпрограмма.clean, так что вы можете увидеть следующий вывод, только один остался в каждом раундеmainсопрограммы.

Вывод программы:

#Goroutines in roop end: 1.
#Goroutines in roop end: 1.
#Goroutines in roop end: 1.
#Goroutines in roop end: 1.

Крайний срок выхода

Помимо ручного вызова функции отмены для выхода, стандартная библиотека также предоставляет две ограниченные по времени операции выхода:WithDeadline(parent Context, d time.Time)а такжеWithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)Функция, вы можете передать момент времени или период времени, указывая, что контекст контекста автоматически вызывается по истечении времени.Done()функция.

Пример использования:
Извлекаем демо с официального сайта:

func main() {
    // 由于传入时间为50微妙,因此在select选择块中,ctx.Done()分支会先执行
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()
    
    select {
    case <-time.After(1 * time.Second):
    	fmt.Println("overslept")
    case <-ctx.Done():
    	fmt.Println(ctx.Err()) // prints "context deadline exceeded"
    }
}
//输出: context deadline exceeded

Истекло время запроса

О контекстеWithTimeout()Использование функции также может судить оhttpВремя ожидания запроса истекло.

Пример программы:

func TestTimeReqWithContext(t *testing.T) {
    //初始化http请求
    request, e := http.NewRequest("GET", "https://www.pixelpigpigpig.xyz", nil)
    if e != nil {
    	log.Println("Error: ", e)
    	return
    }
    
    //使用Request生成子上下文, 并且设置截止时间为10毫秒
    ctx, cancelFunc := context.WithTimeout(request.Context(), 10*time.Millisecond)
    defer cancelFunc()
    
    //绑定超时上下文到这个请求
    request = request.WithContext(ctx)
    
    //time.Sleep(20 * time.Millisecond)
    
    //发起请求
    response, e := http.DefaultClient.Do(request)
    if e != nil {
    	log.Println("Error: ", e)
    	return
    }
    
    defer response.Body.Close()
    //如果请求没问题, 打印body到控制台
    io.Copy(os.Stdout, response.Body)
}

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

=== RUN   TestTimeReqWithContext
2020/05/16 23:17:14 Error:  Get https://www.pixelpigpigpig.xyz: context deadline exceeded

Значение контекста для чтения и записи

Как было показано ранее,contextТакже предоставляется сигнатура функции чтения-записи:

// 上下文值存储字典
Value(key interface{}) interface{}

Что касается его использования, я уже упоминал его в статье «Давайте поговорим о httpRouter».

Пример:

// 获取顶级上下文
ctx := context.Background()
// 在上下文写入string值, 注意需要返回新的value上下文
valueCtx := context.WithValue(ctx, "hello", "pixel")
value := valueCtx.Value("hello")
if value != nil {
    fmt.Printf("Params type: %v, value: %v.\n", reflect.TypeOf(value), value)
}

context.Background()— это непустой контекст верхнего уровня, который не будет отменен, пока программа все еще находится в нем, и не имеет встроенных значений, часто используемых основной функцией.
оcontextВ спецификации Go существует соглашение о том, что его не следует использовать для инкапсуляции долгоживущих параметров.Как правило, он используется только для передачи значения в области запроса.Бизнес-параметры программы должны быть выставлены и помещены в функцию список параметров, чтобы улучшить читаемость.


Область контекста

вышеперечисленноеContextМожно сказать, что несколько применений относительно распространены. Я принимал его раньше.goсерединаcontextаналогияJava SpringсерединаapplicationContextГлобальный контекст, но, строго говоря, это на самом деле спорно. потому чтоSpringКонтекст присутствует на протяжении всего жизненного цикла всей программы и часто имеет некоторые глобальные настройки.go, скорее всего, будет использоваться для управления блоком подпрограммы, иSpringСравнение глобального контекста ,Goизcontextявляется относительно недолговечным.

Внутри Гоcontext.Contextобщие сценарии:

  • Фрагмент задачи, используемый для запуска подпрограммы, такой как выше, используемый для управления выходом подпрограммы.
  • Или представить запрос в сетевом фрейме и управлять его началом до конца, как вGinиз рамыRequestприбытьResponse, а не глобальный.

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

Use context values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions

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

Ссылка на ссылку

Goroutine leak
medium.com/gowolfspec/…
Understanding the context package
medium.com/например, НПО/под…
Context Package Semantics In Go
Woohoo. AR, но labs.com/blog/2019/0…
Context should go away for Go 2(«Затопление контекста Tucai on Go»)
Препятствование криминалистическому тестированию.GitHub.IO/post/con TeX…
Go: Context and Cancellation by Propagation
medium.com/ah-journey-i…
ссылка на изображение: (Иллюстрация, созданная для «Путешествия с Го», сделанная из оригинального Go Gopher, созданного Рене Френч.)