Go言語におけるContextの役割と使い方を詳しく解説

KDP (Data Service Platform) は、KaiwuDB が独自に開発したデータ サービス製品で、KaiwuDB を核として、産業用モノのインターネット、デジタル エネルギー、車両のインターネット、スマート産業に対応する、AIoT シナリオ向けのワンストップ データ サービス プラットフォームです。コア ビジネス シナリオにおけるデータの収集、処理、計算、分析、適用に関する包括的なビジネス要件により、「ビジネスはデータ、データはサービス」を実現し、企業がデータからより大きなビジネス価値を発掘できるように支援します。

データ サービス プラットフォームのリアルタイム コンピューティング コンポーネントを開発する場合、リアルタイム コンピューティング コンポーネントがユーザーにカスタム ルールの機能を提供し、ユーザーが複数のルールを登録して一定期間実行した後、このような問題が発生することがあります。を実行し、ルールの定義を変更して再起動すると、コルーチン リークが発生する問題が発生します。

1. 実際のケース

この記事では、疑似コードを使用して、データ サービス プラットフォームのリアルタイム コンピューティング コンポーネントの開発プロセスで遭遇する可能性のあるコルーチン リークの問題を包括的に紹介します。

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

上記のDummyRuleは、本例のルールデータ構造であり、複数のデータソースSource、複数のデータターゲットSink、データフローFlowを含む。ルールの具体的なプロセスは次のとおりです。

1 と 2 は 2 つのソースであり、最初に 1 と 2 の 2 つのソースを加算によってそれぞれ処理し、次に Merge オペレーションを呼び出してストリームを合成し、次に Fanout オペレーションを実行して 2 つの同一のストリームを生成し、それぞれ 7 と 8 に流れます。 pass 7 と 8 の数値型は文字列に変換され、それぞれ out1.txt ファイルと out2.txt ファイルに書き込まれます。

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

上の図は Source クラス データ ソースの疑似コードです。コンシューマはファイル データを読み取るために使用されるリーダー、out は次のデータ ストリームに渡すために使用されるチャネル、ctx は Go のコンテキストです。これはコンシューマがファイル データを読み取るための別のコルーチンであり、読み取られたデータは出力され、次のデータ ストリームの消費を待ちます。

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

上の図は Sink クラス データ オブジェクトの疑似コードです。プロデューサーはファイルの書き込みに使用されるライターです。in は前のデータ ストリームを受け入れるために使用されるチャネルです。ctx は Go のコンテキストです。プロデューサーがファイルに書き込むデータも別のものです。コルーチン 。

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

上図はデータフロー転送のソースコードです。FlatMapの使い方は curFlow := prevFlow.Via(nextFlow) となっており、前のフローを次のフローに渡すことができ、コルーチン内でデータフローの転送処理が行われていることが分かります。

前のソース コードから、このサンプル ルールには少なくとも 10 個のコルーチンがあることがわかりますが、実際には 10 個をはるかに超えるコルーチンが存在します。データ サービス プラットフォームのリアルタイム コンピューティング コンポーネントでは、コルーチンの管理が非常に複雑であることがわかります。

go pprof、top、go traces などのツールを使用してテストと調査を繰り返した結果、コルーチン リークはルール内のシンクのコンテキストの誤ったキャンセルが原因であることがわかりました。

コンテキストは、ゴルーチンを管理するための重要な言語機能です。Context を正しく使用する方法を学ぶと、ゴルーチン間の関係をより明確にして管理できるようになります。上記の例から、コンテキストの重要性がわかります。コンテキストの正しい使用方法を学習すると、コードの品質が向上するだけでなく、多くのコルーチン リーク調査作業を回避できます。

2、コンテキストの中に入る

1 はじめに

コンテキストは通常​​コンテキストと呼ばれますが、Go 言語ではゴルーチンの実行状態やシーンとして解釈され、上位ゴルーチンと下位ゴルーチンの間で Context の受け渡しがあり、上位ゴルーチンから下位ゴルーチンに Context が渡されます。

各ゴルーチンを実行する前に、プログラムの現在の実行状態を事前に知る必要があり、通常、これらの状態は Context 変数にカプセル化され、実行されるゴルーチンに渡されます。

ネットワーク プログラミングでは、ネットワーク リクエスト Request を受信して​​処理するときに、複数のゴルーチンで処理されることがあります。これらのゴルーチンはリクエストの一部の情報を共有する必要がある場合があり、リクエストがキャンセルまたはタイムアウトすると、このリクエストから作成されたすべてのゴルーチンも終了します。

Go Context パッケージは、プログラム ユニット間で状態変数を共有する方法を実装するだけでなく、呼び出されるプログラム ユニットの外側にある ctx 変数の値を簡単な方法で設定することで、期限切れや取り消しなどのシグナルを呼び出されるプログラムに渡すこともできます。

ネットワーク プログラミングでは、A が B の API を呼び出し、B が C の API を呼び出した場合、A が B を呼び出してキャンセルすると、B による C への呼び出しもキャンセルされる必要があります。Context パッケージを使用すると、リクエストのゴルーチン間でリクエスト データ、キャンセル信号、タイムアウト情報を渡すのが非常に便利になります。

Context パッケージの中核は 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. 使用方法

ゴルーチンの場合、その作成と呼び出しの関係は常に、ツリー構造のようなレイヤーごとの呼び出しのようなものであり、最上位のコンテキストには、下位のゴルーチンの実行をアクティブに閉じる方法が必要です。この関係を実現するために、Context もツリー構造になっており、リーフ ノードは常にルート ノードから派生します。

コンテキスト ツリーを作成するには、最初の手順としてルート ノードを取得する必要があります。Context.Backupgroup 関数の戻り値はルート ノードです。

func Background() Context{
    return background
}

この関数は空のコンテキストを返します。これは通常、リクエストを受信する最初のゴルーチンによって作成され、受信リクエストに対応するコンテキストのルート ノードです。キャンセルできず、値も有効期限もありません。多くの場合、彼はリクエストを処理するトップレベルのコンテキストとして存在します。

ルート ノードを使用すると、子孫ノードを作成できます。Context パッケージには、子孫ノードを作成するための一連のメソッドが用意されています。

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 {}

この関数は Context タイプの親を受け取り、Context タイプの値を返すため、レイヤーごとに異なる Context が作成され、子ノードは親ノードのコピーから取得され、子ノードの一部のステータス値は受け取ったパラメータに従って設定し、子ノードを下位のゴルーチンに渡すことができます。

変更された状態をコンテキスト経由で渡すにはどうすればよいですか?

親ゴルーチンでは、Withxxメソッドを通じてcancelメソッドを取得することができ、子Contextの操作権を獲得します。

(1)キャンセルあり

 WithCancel 関数は、親ノードを子ノードにコピーし、次のように定義される追加の CancelFunc 関数型変数を返します。 type CancelFunc func()

CancelFunc を呼び出すと、対応する子 Context オブジェクトがキャンセルされます。親ゴルーチンでは、WithCancelで子ノードのContextを作成でき、子ゴルーチンのコントロールも取得できます。CancelFunc関数を実行したら、子ノードのContextは終了です。子ノードには次のコードが必要です。終了したかどうかを判断し、 goroutine を終了します。

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

(2)締切あり

 WithDeadline の機能は WithCancel と似ており、親ノードを子ノードにコピーしますが、その有効期限は期限と親の有効期限によって決まります。親の有効期限が期限より早い場合、返される有効期限は親の有効期限と同じになります。親ノードの有効期限が切れたら、すべての子孫ノードを同時に閉じる必要があります。

(3)タイムアウトあり

WithTimeout 関数は WithDeadline に似ていますが、渡す内容が今後の Context の残りの有効期間である点が異なります。どちらも、作成された子 Context の制御 (CancelFunc 型の関数変数) を返します。

最上位の Request リクエスト関数が終了すると、ある Context をキャンセルすることができ、子孫の goroutine が select ctx.Done() に従って終了を判断します。

(4)価値あり

WithValue 関数では、親のコピーを返し、このコピーの Value(key) メソッドを呼び出して値を取得します。このようにして、ルート ノードの元の値を保持するだけでなく、子孫ノードにも新しい値を追加します。同じキーが存在する場合は上書きされることに注意してください。

3. 例

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)        
        }
}

このプロセスのコンテキストのアーキテクチャ図は次のとおりです。

[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

今回のワーカーの終了は、ctxWithCancel のタイマーの満了が原因であることがわかります。

マネージャーの継続時間を 2 秒に変更し、WithTimeout の継続時間を変更せずに再度実行します。ワーカーは 2 秒間だけ動作し、その後マネージャーによって事前に停止されました。

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

おすすめ

転載: blog.csdn.net/ZNBase/article/details/131410274