Entretien Golang : contexte en golang (4)


titre : context in golang (4)
auther : Russshare
toc : true
date : 2021-07-13 19:19:30
tags : [interview, golang, context]
catégories : golang interview

Avant-propos :

Récemment, j'étudiais les coroutines. J'ai vu des informations disant que dans un environnement réseau complexe avec une forte concurrence, le canal est difficile à répondre à nos exigences d'utilisation, et nous devons utiliser le contexte. J'ai également vu cela dans le cas précédent. Cela semble vraiment important, j'ai donc appris deux articles Blog très puissant pour apprendre.

https://studygolang.com/articles/9517
https://leileiluoluo.com/posts/golang-context.html

texte

Introduction

Créer une nouvelle goroutine dans golang ne renvoie pas un pid similaire au langage c, donc
on ne peut pas tuer une goroutine de l'extérieur, donc je dois la laisser se terminer toute seule,
on a utilisé channel + select avant, pour résoudre ce problème, mais certains scénarios sont lourds à implémenter.Par
exemple, les goroutines dérivées d'une requête doivent respecter certaines contraintes
pour implémenter des fonctions telles que la période de validité, l'abandon de l'arbre de routine et le transfert des variables globales de la requête.
Google nous a donc fourni une solution et a ouvert le package de contexte. Pour utiliser context pour implémenter le contrat de fonction de contexte, vous devez passer une variable de type context.Context
comme premier paramètre de votre method .

1. Scène

Nous savons que sur le serveur Go, chaque requête entrante sera traitée par sa propre goroutine.

Par exemple, dans le code suivant, pour chaque requête, Handler créera une goroutine pour lui fournir des services , et pour trois requêtes consécutives, l'adresse de r est également différente.

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
        fmt.Println(&r)
        w.Write([]byte("hello"))
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}
$ go run test.go


$ curl http://localhost:8080/echo
$ curl http://localhost:8080/echo
$ curl http://localhost:8080/echo
0xc000072040
0xc000072048
0xc000072050

Le gestionnaire correspondant à chaque requête démarre souvent des goroutines supplémentaires pour les requêtes de données ou les appels PRC.

Lorsque la demande revient, ces goroutines créées en plus doivent être recyclées à temps.
De plus, une requête correspond à un ensemble de données dans le champ de requête qui peut être requis par chaque goroutine dans la chaîne d'appel de requête.

Par exemple, dans le code suivant, lorsqu'une requête arrive,
Handler créera une goroutine de surveillance, qui imprimera une phrase "req est en cours de traitement" toutes les 1s.

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
)

func main() {
    http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
        // monitor
        go func() {
            for range time.Tick(time.Second) {
                fmt.Println("req is processing")
            }
        }()

        // assume req processing takes 3s
        time.Sleep(3 * time.Second)
        w.Write([]byte("hello"))
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}

En supposant que la requête prend 3 secondes, c'est-à-dire que la requête revient après 3 secondes, nous nous attendons à ce que la goroutine de surveillance s'arrête après avoir affiché "req est en cours de traitement" 3 fois.

Cependant, après l'exécution, il s'avère qu'après que la goroutine de surveillance imprime 3 fois, elle ne se terminera pas, mais continuera à imprimer.

Le problème est qu'après la création de la goroutine de surveillance , son cycle de vie n'est pas contrôlé. Ensuite, nous utilisons le contexte pour la contrôler , c'est-à-dire qu'avant que le programme de surveillance ne s'imprime, il est nécessaire de vérifier si r.Context() s'est terminé. S'il se termine, quittez la boucle, c'est-à-dire terminez le cycle de vie .

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
)

func main() {
    http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
        // monitor
        go func() {
            for range time.Tick(time.Second) {
                select {
                case <-r.Context().Done():
                    fmt.Println("req is outgoing")
                    return
                default:
                    fmt.Println("req is processing")
                }
            }
        }()

        // assume req processing takes 3s
        time.Sleep(3 * time.Second)
        w.Write([]byte("hello"))
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Sur la base des exigences ci-dessus, l'application de package de contexte est née.

Le package de contexte peut fournir une requête depuis la limite de requête API vers le transfert de données de domaine de requête, le signal d'annulation et l'échéance de chaque goroutine.

2 Type de contexte

// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

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

La méthode Done renvoie un canal qui sera fermé lorsque le contexte est annulé ou que la date limite est atteinte. La méthode Err renvoie la raison de l'annulation du contexte.

Le contexte lui-même n'a pas de méthode Cancel, et le canal Done n'est utilisé que pour recevoir des signaux : la fonction qui reçoit le signal d'annulation ne doit pas être la fonction qui envoie le signal d'annulation en même temps. La goroutine parent démarre la goroutine enfant pour effectuer certaines opérations enfant, et la goroutine enfant ne doit pas être utilisée pour annuler la goroutine parent.

Le contexte est sûr et peut être utilisé par plusieurs goroutines en même temps. Un contexte peut être transmis à plusieurs goroutines et des signaux d'annulation peuvent être envoyés à toutes ces goroutines.

S'il existe une échéance, la méthode Deadline peut renvoyer l'heure d'annulation du Context.

La valeur permet à Context de transporter des données dans le domaine de la demande, et l'accès aux données doit garantir la sécurité d'un accès simultané par plusieurs goroutines.

Contexte dérivé

// 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.
func Background() Context

// WithCancel returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed or cancel is called.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// A CancelFunc cancels a Context.
type CancelFunc func()

// A CancelFunc cancels a 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.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

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

Le package de contexte offre la possibilité de dériver de nouveaux contextes à partir de contextes existants. De cette manière, une arborescence de contextes peut être formée.
Lorsque le contexte parent est annulé, tous les contextes enfants qui en sont dérivés seront également annulés.

L'arrière-plan est la racine de tous les arbres de contexte et il ne peut jamais être annulé.

WithCancel et WithTimeout peuvent être utilisés pour créer un contexte dérivé, WithCancel peut être utilisé pour annuler un groupe de goroutines dérivées de celui-ci, et WithTimeout peut être utilisé pour définir un délai.

WithValue fournit à Context la possibilité de demander des données de domaine.

Examinons quelques exemples d'utilisation des méthodes ci-dessus.

1) Tout d'abord, regardez l'utilisation de WitchCancel.

Dans le code ci-dessous, la fonction principale utilise WithCancel pour créer un ctx basé sur l'arrière-plan .

Ensuite, démarrez une goroutine de moniteur, le moniteur imprime "monitor working" toutes les 1s,

La fonction principale exécute l'annulation après 3 s, puis le moniteur s'arrêtera après avoir détecté le signal d'annulation.

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // monitor
    go func() {
        for range time.Tick(time.Second) {
            select {
            case <-ctx.Done():
                return
            default:
                fmt.Println("monitor woring")
            }
        }
    }()

    time.Sleep(3 * time.Second)
}
2) Regardez un autre exemple d'utilisation de WithTimeout,

Le code suivant utilise WithTimeout pour créer un ctx basé sur l'arrière-plan, qui sera annulé après 3 secondes.

Notez que bien qu'il soit automatiquement annulé avant la date limite, il est toujours recommandé d'ajouter le code d'annulation.

Qu'il soit annulé avant la date limite ou par le code d'annulation dépend du signal envoyé plus tôt.

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    select {
    case <-time.After(4 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err())
    }
}

L'utilisation de WithDeadline est similaire à WithTimeout.

Si vous ne pensez pas à l'utilisation spécifique de Context, vous pouvez utiliser TODO pour occuper l'espace, et il est également pratique pour les outils de vérifier l'exactitude.

3) Enfin, regardez l'utilisation de WithValue.

Le code suivant crée un ctx avec une valeur basée sur l'arrière-plan, puis la valeur peut être obtenue en fonction de la clé.

Remarque : Pour éviter les conflits causés par plusieurs packages utilisant le contexte en même temps, il n'est pas recommandé d'utiliser une chaîne ou d'autres types intégrés pour la clé, mais de personnaliser le type de clé.

package main

import (
    "context"
    "fmt"
)

type ctxKey string

func main() {
    ctx := context.WithValue(context.Background(), ctxKey("a"), "a")

    get := func(ctx context.Context, k ctxKey) {
        if v, ok := ctx.Value(k).(string); ok {
            fmt.Println(v)
        }
    }
    get(ctx, ctxKey("a"))
    get(ctx, ctxKey("b"))
}

Enfin, listez les règles d'utilisation du contexte :

a) N'utilisez pas Context comme champ de struct, mais utilisez-le comme paramètre pour chaque fonction qui l'utilise, qui doit être défini comme le premier paramètre d'une fonction ou d'une méthode, généralement appelé ctx ;

b) Ne passez pas nil au paramètre Context, si vous n'avez pas pensé à utiliser ce Context, veuillez passer context.TODO;

c) L'utilisation du contexte pour transmettre des valeurs ne peut être utilisée que comme données dans le champ de requête, veuillez ne pas abuser d'autres types de données ;

d) Le même contexte peut être transmis à plusieurs goroutines qui l'utilisent, et le contexte peut être accessible en toute sécurité par plusieurs goroutines en même temps.

Enfin, répétez en analysant le code source

interface context.Context

Le cœur du package de contexte

//  context 包里的方法是线程安全的,可以被多个 goroutine 使用    
type Context interface {               
    // 当Context 被 canceled 或是 times out 的时候,Done 返回一个被 closed 的channel      
    Done() <-chan struct{}        

    // 在 Done 的 channel被closed 后, Err 代表被关闭的原因   
    Err() error 

    // 如果存在,Deadline 返回Context将要关闭的时间  
    Deadline() (deadline time.Time, ok bool)

    // 如果存在,Value 返回与 key 相关了的值,不存在返回 nil  
    Value(key interface{}) interface{}
}

Nous n'avons pas besoin d'implémenter manuellement cette interface, le package context nous en a fourni deux,
l'un est Background(), l'autre est TODO(), les deux fonctions renverront une instance de Context.
C'est juste que les deux instances retournées sont vides Context.

L'ossature principale

La structure cancelCtx hérite de Context et implémente la méthode d'annulation :

//*cancelCtx 和 *timerCtx 都实现了canceler接口,实现该接口的类型都可以被直接canceled
type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}        

type cancelCtx struct {
    Context
    done chan struct{} // closed by the first cancel call.
    mu       sync.Mutex
    children map[canceler]bool // set to nil by the first cancel call
    err      error             // 当其被cancel时将会把err设置为非nil
}

func (c *cancelCtx) Done() <-chan struct{} {
    return c.done
}

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

func (c *cancelCtx) String() string {
    return fmt.Sprintf("%v.WithCancel", c.Context)
}

//核心是关闭c.done
//同时会设置c.err = err, c.children = nil
//依次遍历c.children,每个child分别cancel
//如果设置了removeFromParent,则将c从其parent的children中删除
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
    close(c.done)
    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 {
        removeChild(c.Context, c) // 从此处可以看到 cancelCtx的Context项是一个类似于parent的概念
    }
}

La structure timerCtx hérite de cancelCtx

type timerCtx struct {
    cancelCtx //此处的封装为了继承来自于cancelCtx的方法,cancelCtx.Context才是父亲节点的指针
    timer *time.Timer // Under cancelCtx.mu. 是一个计时器
    deadline time.Time
}

La structure valueCtx hérite de cancelCtx

type valueCtx struct {
    Context
    key, val interface{}
}
méthode principale
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 interface{}, val interface{}) Context


WithCancel correspond à cancelCtx, qui renvoie à la fois un cancelCtx et un CancelFunc.
CancelFunc est un type de fonction défini dans le package de contexte : type CancelFunc func().
Lors de l'appel de cette CancelFunc, fermez le c.done correspondant, c'est-à-dire laissez sa goroutine descendante se terminer.

WithDeadline et WithTimeout correspondent à timerCtx. WithDeadline et WithTimeout sont similaires.
WithDeadline consiste à définir une heure limite spécifique. Lorsque la date limite est atteinte, la goroutine descendante se termine,
tandis que WithTimeout est simple et grossier, renvoie directement WithDeadline(parent, time.Now( ).Ajouter (délai d'attente)).

WithValue correspond à valueCtx. WithValue définit une carte dans le contexte, et
les goroutines qui obtiennent le contexte et ses descendants peuvent obtenir la valeur dans la carte.

Interprétation détaillée du code source du package de contexte : aller à l'interprétation du code source

Principes d'utilisation

Les packages utilisant Context doivent suivre les principes suivants pour respecter la cohérence de l'interface et faciliter l'analyse statique

Ne stockez pas le contexte dans une structure et transmettez-le explicitement à la fonction. La variable Context doit être utilisée comme premier paramètre, généralement nommé ctx

Même si la méthode le permet, ne passez pas un Context nil, si vous n'êtes pas sûr du Context que vous voulez utiliser, passez un context.TODO

Les méthodes liées à la valeur utilisant le contexte ne doivent être utilisées que pour les métadonnées liées à la demande transmises dans les programmes et les interfaces, ne les utilisez pas pour transmettre certains paramètres facultatifs

Le même contexte peut être utilisé pour passer à différentes goroutines, le contexte est sûr dans plusieurs goroutines

Exemple d'utilisation

L'exemple est copié de : Introduction au package de contexte dans Golang

package main

import (
    "fmt"
    "time"
    "golang.org/x/net/context"
)

// 模拟一个最小执行时间的阻塞函数
func inc(a int) int {
    res := a + 1                // 虽然我只做了一次简单的 +1 的运算,
    time.Sleep(1 * time.Second) // 但是由于我的机器指令集中没有这条指令,
    // 所以在我执行了 1000000000 条机器指令, 续了 1s 之后, 我才终于得到结果。B)
    return res
}

// 向外部提供的阻塞接口
// 计算 a + b, 注意 a, b 均不能为负
// 如果计算被中断, 则返回 -1
func Add(ctx context.Context, a, b int) int {
    res := 0
    for i := 0; i < a; i++ {
        res = inc(res)
        select {
        case <-ctx.Done():
            return -1
        default:
        }
    }
    for i := 0; i < b; i++ {
        res = inc(res)
        select {
        case <-ctx.Done():
            return -1
        default:
        }
    }
    return res
}

func main() {
    {
        // 使用开放的 API 计算 a+b
        a := 1
        b := 2
        timeout := 2 * time.Second
        ctx, _ := context.WithTimeout(context.Background(), timeout)
        res := Add(ctx, 1, 2)
        fmt.Printf("Compute: %d+%d, result: %d\n", a, b, res)
    }
    {
        // 手动取消
        a := 1
        b := 2
        ctx, cancel := context.WithCancel(context.Background())
        go func() {
            time.Sleep(2 * time.Second)
            cancel() // 在调用处主动取消
        }()
        res := Add(ctx, 1, 2)
        fmt.Printf("Compute: %d+%d, result: %d\n", a, b, res)
    }
}

Guess you like

Origin blog.csdn.net/weixin_45264425/article/details/132200034