ホームページ  >  記事  >  バックエンド開発  >  Go言語でのContextの詳しい説明

Go言語でのContextの詳しい説明

Guanhui
Guanhui転載
2020-06-18 17:54:173759ブラウズ

Go言語でのContextの詳しい説明

#1. コンテキストとは何ですか?

Go 1.7 より前では、コンテキストはまだコンパイルされておらず、golang.org/x/net/context パッケージに存在していました。

その後、Golang チームはコンテキストが非常に使いやすいことに気づき、Go 1.7 の標準ライブラリにコンテキストを組み込みました。

Context (コンテキストとも呼ばれます) のインターフェイスは次のように定義されています

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

Context インターフェイスには 4 つのメソッドがあることがわかります

  • Deadline: 最初に返される値は 期限時刻です。この時点で、コンテキストは自動的にキャンセル アクションをトリガーします。返される 2 番目の値はブール値です。true は期限が設定されていることを意味し、false は期限が設定されていないことを意味します。期限が設定されていない場合は、cancel 関数を手動で呼び出してコンテキストをキャンセルする必要があります。
  • Done: struct{} 型の読み取り専用チャネル (キャンセルされた後にのみ返されます) を返します。このチャネルが読み取り可能な場合、親コンテキストがキャンセル要求を開始したことを意味し、開発者はこの信号に基づいていくつかのクリーンアップ アクションを実行し、ゴルーチンを終了できます。
  • Err: コンテキストがキャンセルされた理由を返します。
  • Value: キーと値のペアであるコンテキストにバインドされた値を返します。そのため、対応する値はキーを通じて取得できます。この値は通常、スレッドセーフです。

#2. コンテキストはなぜ必要なのでしょうか?

Goroutine が開かれている場合、それを強制的に閉じることはできません。

コルーチンを閉じる一般的な理由は次のとおりです。

  1. ゴルーチンが実行を終了し、自動的に終了します。
  2. メイン プロセスがクラッシュして終了し、ゴルーチンが強制的に終了します。
  3. コルーチンのシャットダウンをガイドするために、チャネルを通じて信号を送信します。

最初のタイプは通常のシャットダウンであり、今日の説明の範囲外です。

2 番目のタイプは異常シャットダウンであり、コードを最適化する必要があります。

3 番目の方法は、開発者がコルーチンを手動で制御できることです。コード例は次のとおりです:

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 chan を定義します。バックグラウンドのゴルーチンを終了します。実装も非常にシンプルです。バックグラウンドのゴルーチンで、select を使用して stop が値を受信できるかどうかを判断します。受信できた場合は、終了して停止できることを意味します。受信できなかった場合は、 default が実行されます。 の監視ロジックは、stop 通知を受信するまで監視を続けます。

上記は goroutine のシナリオですが、複数の goroutine があり、各 goroutine の下で複数の goroutine シナリオが開かれている場合はどうなるでしょうか。 Context を使用する理由に関する Feixue Wuqing のブログで、彼は次のように述べています:

chan select メソッドは、ゴルーチンを終了するより洗練された方法ですが、このメソッドにも制限があります。それを終了するには制御する必要がありますか?これらのゴルーチンが他のゴルーチンを生成したらどうなるでしょうか?ゴルーチンの層が無限にある場合はどうなるでしょうか?これは非常に複雑で、多くの chan を定義したとしても、ゴルーチンの関係チェーンによってこのシナリオが非常に複雑になるため、この問題を解決するのは困難です。

私はここでの彼の意見にはあまり同意できません。なぜなら、チャネルを 1 つだけ使用しても、複数の goroutine を制御 (キャンセル) するという目的は達成できると思うからです。例を使って確認してみましょう。

この例の原理は次のとおりです。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 を使用してチャネルを閉じることができることを示しています。すべてのゴルーチンでチャネルが閉じているかどうかを継続的に確認します (前提として、チャネルを閉じるだけで他のデータは送信しないという合意を結ぶ必要があります。そうしないと、データを一度送信するとゴルーチンが閉じられてしまい、期待を満たせません。このチャネルはカプセル化され制限されています) を使用して goroutine を終了するかどうかを決定するのが最善です。

つまり、ご覧のとおり、初心者として、コンテキストを使用する必然的な理由がまだ見つかりません。コンテキストは非常に便利なものであるとしか言えません。コンテキストを使用すると、対処が容易になります同時実行性を扱うときにいくつかの問題がありますが、必須ではありません。

言い換えれば、ができるかどうかという問題ではなく、を使用したほうがよいという問題が解決されます。

3. 単純にコンテキストを使用する

上記のチャネルを閉じる方法を使用しない場合、それを実現する他のより洗練された方法はありますか?

はい、これがこの記事のコンテキストに関する内容です。

コンテキストを使用して上記の例を変更しました。

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("主程序退出!!")

}

ここのキーコードはわずか 3 行です

第一行:以 context.Background() 为 parent context 定义一个可取消的 context

ctx, cancel := context.WithCancel(context.Background())

第二行:然后你可以在所有的goroutine 里利用 for + select 搭配来不断检查 ctx.Done() 是否可读,可读就说明该 context 已经取消,你可以清理 goroutine 并退出了。

case <- ctx.Done():

第三行:当你想到取消 context 的时候,只要调用一下 cancel 方法即可。这个 cancel 就是我们在创建 ctx 的时候返回的第二个值。

cancel()

运行结果输出如下。可以发现我们实现了和 close 通道一样的效果。

监控器3,正在监控中...
监控器4,正在监控中...
监控器1,正在监控中...
监控器2,正在监控中...
监控器2,接收到通道值为:{},监控结束。
监控器5,接收到通道值为:{},监控结束。
监控器4,接收到通道值为:{},监控结束。
监控器1,接收到通道值为:{},监控结束。
监控器3,接收到通道值为:{},监控结束。
主程序退出!!

4. 根Context 是什么?

创建 Context 必须要指定一个 父 Context,当我们要创建第一个Context时该怎么办呢?

不用担心,Go 已经帮我们实现了2个,我们代码中最开始都是以这两个内置的context作为最顶层的parent context,衍生出更多的子Context。

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

func Background() Context {
    return background
}

func TODO() Context {
    return todo
}

一个是Background,主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context,它不能被取消。

一个是TODO,如果我们不知道该使用什么Context的时候,可以使用这个,但是实际应用中,暂时还没有使用过这个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. Context 的继承衍生

上面在定义我们自己的 Context 时,我们使用的是 WithCancel 这个方法。

除它之外,context 包还有其他几个 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

这四个函数有一个共同的特点,就是第一个参数,都是接收一个 父context。

通过一次继承,就多实现了一个功能,比如使用 WithCancel 函数传入 根context ,就创建出了一个子 context,该子context 相比 父context,就多了一个 cancel context 的功能。

如果此时,我们再以上面的子context(context01)做为父context,并将它做为第一个参数传入WithDeadline函数,获得的子子context(context02),相比子context(context01)而言,又多出了一个超过 deadline 时间后,自动 cancel context 的功能。

接下来我会举例介绍一下这几种 context,其中 WithCancel 在上面已经讲过了,下面就不再举例了

例子 1:WithDeadline

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

WithTimeout 和 WithDeadline 使用方法及功能基本一致,都是表示超过一定的时间会自动 cancel context。

唯一不同的地方,我们可以从函数的定义看出

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:WithValue

通过Context我们也可以传递一些必须的元数据,这些数据会附加在Context上以供使用。

元数据以 Key-Value 的方式传入,Key 必须有可比性,Value 必须是线程安全的。

还是用上面的例子,以 ctx02 为父 context,再创建一个能携带 value 的ctx03,由于他的父context 是 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 使用注意事项

  1. 通常 Context 都是做为函数的第一个参数进行传递(规范性做法),并且变量名建议统一叫 ctx
  2. Context 是线程安全的,可以放心地在多个 goroutine 中使用。
  3. 当你把 Context 传递给多个 goroutine 使用时,只要执行一次 cancel 操作,所有的 goroutine 就可以收到 取消的信号
  4. 不要把原本可以由函数参数来传递的变量,交给 Context 的 Value 来传递。
  5. 当一个函数需要接收一个 Context 时,但是此时你还不知道要传递什么 Context 时,可以先用 context.TODO 来代替,而不要选择传递一个 nil。
  6. 当一个 Context 被 cancel 时,继承自该 Context 的所有 子 Context 都会被 cancel。

推荐教程:《Go教程

以上がGo言語でのContextの詳しい説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.imで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。