Explication détaillée du rôle et de l'utilisation de Context dans le langage Go

KDP (Data Service Platform) est un produit de service de données développé indépendamment par KaiwuDB. Avec KaiwuDB comme noyau, il s'agit d'une plate-forme de service de données à guichet unique pour les scénarios AIoT, répondant à l'Internet industriel des objets, à l'énergie numérique, à l'Internet des véhicules, aux industries intelligentes. Exigences commerciales complètes pour la collecte, le traitement, le calcul, l'analyse et l'application des données dans les scénarios d'activité de base, en réalisant que "l'entreprise est la donnée, la donnée est le service" et en aidant les entreprises à exploiter une plus grande valeur commerciale à partir des données.

Lors du développement du composant de calcul en temps réel de la plate-forme de service de données, vous pouvez rencontrer un problème : le composant de calcul en temps réel fournit aux utilisateurs la fonction de règles personnalisées. Une fois que l'utilisateur a enregistré plusieurs règles et les a exécutées pendant un certain temps , puis modifie la définition des règles et redémarre, le problème se produit. Une fuite de coroutine s'est produite.

1. Cas réel

Cet article utilisera le pseudocode pour présenter de manière exhaustive le problème de fuite de coroutine qui peut être rencontré dans le processus de développement de composants informatiques en temps réel de la plate-forme de service de données.

//规则的大致数据结构
type DummyRule struct{
    BaseRule    
    sorce []Source    
    sink  []Sink    
    //flow map key:flow 名称,value:flow 实例    
    flow map[string]Flow    
    ...
}

La DummyRule ci-dessus est la structure de données de règle de cet exemple, qui comprend plusieurs sources de données Source, plusieurs cibles de données Sinks et un flux de données Flow. Le processus spécifique des règles est le suivant :

1 et 2 sont deux sources, traitez d'abord les deux sources de 1 et 2 respectivement par addition ; deuxièmement appelez l'opération Merge pour synthétiser un flux ; puis effectuez l'opération Fanout pour générer deux flux identiques, qui s'écoulent respectivement dans 7 et 8 ; enfin pass 7 et Le type de nombre de 8 est converti en une chaîne de caractères et écrit respectivement dans les fichiers out1.txt et out2.txt.

type Source struct{
  consumers       []file.reader  
  out             chan interface{}  
  ctx             context.Context  
  cancel          context.CancelFunc  
  ...
}

La figure ci-dessus est le pseudocode de la source de données de la classe Source, les consommateurs sont les lecteurs utilisés pour lire les données du fichier, out est le canal utilisé pour passer au flux de données suivant et ctx est le contexte de Go. Il s'agit d'une coroutine distincte permettant aux consommateurs de lire les données de fichier, et les données lues seront mises en sortie, en attendant la consommation du prochain flux de données.

type Sink struct{
   producers  []file.writer   
   in         chan interface{}   
   ctx        context.Context   
   cancel context.CancelFunc   
   ...
}

La figure ci-dessus est le pseudo-code de l'objet de données de la classe Sink, les producteurs sont les écrivains utilisés pour écrire des fichiers, dans est le canal utilisé pour accepter le flux de données précédent, ctx est le contexte de Go, les producteurs écrivent les données du fichier est également un élément distinct coroutine.

func(fm FlatMap) Via(flow streams.Flow) streams.Flow{
    go fm.transmit(flow)
    return flow
}

La figure ci-dessus est le code source du transfert de flux de données. L'utilisation de FlatMap est curFlow := prevFlow.Via(nextFlow), afin que le Flow précédent puisse être passé au Flow suivant. Vous pouvez voir qu'un processus de transfert de flux de données est effectué dans une coroutine.

D'après le code source précédent, nous pouvons voir qu'il y a au moins 10 coroutines dans cet exemple de règle, mais en fait, il y a bien plus de 10 coroutines. On constate que dans les composants de calcul temps réel de la plate-forme de services de données, la gestion des coroutines est très compliquée.

Après des tests répétés et des enquêtes à l'aide d'outils tels que go pprof, top et go traces, nous avons constaté que la fuite de coroutine était causée par l'annulation incorrecte du contexte du récepteur dans les règles.

Le contexte est une caractéristique importante du langage pour gérer les goroutines. Apprendre à utiliser correctement le contexte peut mieux clarifier et gérer la relation entre les goroutines. À partir des exemples ci-dessus, nous pouvons voir l'importance du contexte. Apprendre à utiliser correctement le contexte peut non seulement améliorer la qualité du code, mais également éviter de nombreux travaux d'enquête sur les fuites de coroutine.

Deux, dans le contexte

1. Introduction

Le contexte est généralement appelé contexte. En langage Go, il est compris comme l'état d'exécution et la scène de la goroutine. Il y a un transfert de contexte entre les goroutines supérieures et inférieures, et les goroutines supérieures transmettront le contexte aux goroutines inférieures.

Avant de s'exécuter, chaque goroutine doit connaître à l'avance l'état d'exécution actuel du programme. Habituellement, ces états sont encapsulés dans une variable de contexte et transmis à la goroutine à exécuter.

Dans la programmation réseau, lorsqu'une demande de demande de réseau est reçue et que la demande est traitée, elle peut être traitée dans plusieurs goroutines. Et ces goroutines peuvent avoir besoin de partager certaines informations de la requête. Lorsque la requête est annulée ou expirée, toutes les goroutines créées à partir de cette requête seront également terminées.

Le package Go Context implémente non seulement la méthode de partage des variables d'état entre les unités de programme, mais peut également transmettre des signaux tels que l'expiration ou la révocation au programme appelé en définissant la valeur de la variable ctx en dehors de l'unité de programme appelée de manière simple. unité.

En programmation réseau, si A appelle l'API de B et B appelle l'API de C, alors si A appelle B pour annuler, alors l'appel de B à C doit également être annulé. Le package Context facilite la transmission des données de requête, des signaux d'annulation et des informations de délai d'attente entre les goroutines de requête.

Le cœur du package Context est l'interface Context :

// A Context carries a deadline, a cancellation signal, and other values across
// API boundaries
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface{     
     // 返回一个超时时间,到期则取消context。在代码中,可以通过deadline为io操作设置超过时间     
     Deadline() (deadline time.Time, ok bool)     
     // 返回一个channel,用于接收context的取消或者deadline信号。     
     // 当channel关闭,监听done信号的函数会立即放弃当前正在执行的操作并返回。     
     // 如果context实例时不可能取消的,那么     
     // 返回nil,比如空context,valueCtx     
     Done()
}

2. Comment utiliser

Pour les goroutines, leur relation de création et d'appel est toujours comme un appel couche par couche, comme une structure arborescente, et le contexte en haut devrait avoir un moyen de fermer activement l'exécution des goroutines subordonnées. Afin de réaliser cette relation, Context est également une structure arborescente et les nœuds feuilles sont toujours dérivés du nœud racine.

Pour créer une arborescence de contexte, la première étape doit être d'obtenir le nœud racine, et la valeur de retour de la fonction Context.Backupgroup est le nœud racine.

func Background() Context{
    return background
}

Cette fonction renvoie un Contexte vide, généralement créé par la première goroutine qui reçoit la requête, et qui est le nœud racine du Contexte correspondant à la requête entrante. Il ne peut pas être annulé, n'a pas de valeur et n'a pas de délai d'expiration. Il existe souvent en tant que contexte de niveau supérieur qui gère la demande.

Avec le nœud racine, vous pouvez créer des nœuds descendants. Le package Context fournit une série de méthodes pour les créer :

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {}
func WithDeadline(parent Context, d time.Time)(Context, CancelFunc) {}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {}
func WithValue(parent Context, key, val interface{}) Context {}

La fonction reçoit un parent de type Contexte et renvoie une valeur de type Contexte, de sorte que différents Contextes sont créés couche par couche, le nœud enfant est obtenu en copiant le nœud parent et certaines valeurs d'état du nœud enfant sont défini en fonction des paramètres reçus, puis Ensuite, le nœud enfant peut être passé à la goroutine inférieure.

Comment passer l'état modifié via Context ?

Dans la goroutine parent, une méthode d'annulation peut être obtenue via la méthode Withxx, obtenant ainsi le droit de faire fonctionner le contexte enfant.

(1)AvecAnnuler

 La fonction WithCancel consiste à copier le nœud parent sur le nœud enfant et à renvoyer une variable supplémentaire de type de fonction CancelFunc, qui est définie comme suit : type CancelFunc func()

L'appel de CancelFunc annulera l'objet Context enfant correspondant. Dans la goroutine parent, le contexte du nœud enfant peut être créé via WithCancel, et le contrôle de la goroutine enfant est également obtenu. Une fois la fonction CancelFunc exécutée, le contexte du nœud enfant est terminé. Le nœud enfant a besoin du code suivant pour déterminer s'il est terminé et quitter la goroutine :

select {
case <- ctx.Cone():
    fmt.Println("do some clean work ...... ")
}

(2)Avec Délai

 La fonction de WithDeadline est similaire à celle de WithCancel. Elle copie également le nœud parent sur le nœud enfant, mais son heure d'expiration est déterminée par l'heure d'expiration de l'échéance et du parent. Lorsque l'heure d'expiration du parent est antérieure à l'échéance, l'heure d'expiration renvoyée est la même que l'heure d'expiration du parent. Lorsque le nœud parent expire, tous les nœuds descendants doivent être fermés en même temps.

(3)AvecTimeout

La fonction WithTimeout est similaire à WithDeadline, sauf que ce qu'il transmet est la durée de vie restante du Context à partir de maintenant. Ils renvoient également tous les deux le contrôle du contexte enfant créé, une variable de fonction de type CancelFunc.

Lorsque la fonction de requête de niveau supérieur se termine, nous pouvons annuler un certain contexte, et la goroutine descendante juge la fin en fonction de select ctx.Done().

(4)Avec valeur

Avec la fonction Value, renvoie une copie du parent, l'appel de la méthode Value(key) de cette copie obtiendra la valeur. De cette manière, non seulement nous conservons la valeur d'origine du nœud racine, mais nous ajoutons également de nouvelles valeurs aux nœuds descendants ; notez que si la même clé existe, elle sera écrasée.

3. Exemples

package main
import (
        "context"        
        "fmt"        
        "time"
)
func main() {
        ctxWithCancel, cancel := context.WithTimeout(context.Background(), 5 * time.Second)                
        
        go worker(ctxWithCancel, "[1]")        
        go worker(ctxWithCancel, "[2]")                
        
        go manager(cancel)                
        
        <-ctxWithCancel.Done()        
        // 暂停1秒便于协程的打印输出        
        time.Sleep(1 * time.Second)        
        fmt.Println("example closed")
}
func manager(cancel func( )) {
        time.Sleep(10 * time.Second)         
        fmt.Println("manager called cancel()")         
        cancel() 
}                
func worker(ctxWithCancle context.Context, name string) {
        for {
                 select {                 
                 case <- ctxWithCancel.Done():                          
                          fmt.Println(name, "return for ctxWithCancel.Done()")                          
                          return                 
                 default:
                          fmt.Println(name, "working")                 
                          }                 
                          time.Sleep(1 * time.Second)        
        }
}

Le schéma d'architecture du Contexte de ce processus :

[1]working
[2]working
[2]working
[1]working
[1]working
[2]working
[2]working
[1]working
[1]working
[2]working
[1]return for ctxWithCancel.Done()
[2]return for ctxWithCancel.Done()example closed

On peut voir que la fin du worker cette fois est causée par l'expiration du timer de ctxWithCancel.

Modifiez la durée du manager à 2 secondes, gardez la durée de WithTimeout inchangée et exécutez-la à nouveau. Le travailleur n'a travaillé que 2 secondes avant d'être arrêté par le manager à l'avance.

[1]working
[2]working
[2]working
[1]workingmanager called cancel()
[1]return for ctxWithCancel.Done()
[2]return for ctxWithCancel.Done()example closed

Je suppose que tu aimes

Origine blog.csdn.net/ZNBase/article/details/131410274
conseillé
Classement