Go 언어의 Context에 대한 자세한 설명

Guanhui
풀어 주다: 2020-06-18 17:54:17
앞으로
3735명이 탐색했습니다.

Go 언어의 Context에 대한 자세한 설명

1.컨텍스트란 무엇인가요?

Go 1.7 이전에는 컨텍스트가 아직 컴파일되지 않았으며 golang.org/x/net/context 패키지에 존재했습니다.

나중에 Golang 팀은 컨텍스트가 사용하기 매우 쉽다는 사실을 발견하여 Go 1.7의 표준 라이브러리에 컨텍스트를 통합했습니다.

context라고도 불리는 Context의 인터페이스는 다음과 같이 정의됩니다

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

Context 인터페이스에는 총 4개의 메소드가 있는 것을 볼 수 있습니다

  • Deadline: 반환된 첫 번째 값은 Deadline:返回的第一个值是 截止时间,到了这个时间点,Context 会自动触发 Cancel 动作。返回的第二个值是 一个布尔值,true 表示设置了截止时间,false 表示没有设置截止时间,如果没有设置截止时间,就要手动调用 cancel 函数取消 Context。
  • Done:返回一个只读的通道(只有在被cancel后才会返回),类型为 struct{}。当这个通道可读时,意味着parent context已经发起了取消请求,根据这个信号,开发者就可以做一些清理动作,退出goroutine。
  • Err:返回 context 被 cancel 的原因。
  • Value:返回被绑定到 Context 的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。

2. 为何需要 Context?

当一个协程(goroutine)开启后,我们是无法强制关闭它的。

常见的关闭协程的原因有如下几种:

  1. goroutine 自己跑完结束退出
  2. 主进程crash退出,goroutine 被迫退出
  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,通知他结束后台goroutine。实现也非常简单,在后台goroutine中,使用select判断stop是否可以接收到值,如果可以接收到,就表示可以退出停止了;如果没有接收到,就会执行default里的监控逻辑,继续监控,只到收到stopdeadline

입니다. 이제 시간이 되었습니다. 이 시점에서 Context는 자동으로 취소 작업을 트리거합니다. 반환된 두 번째 값은 부울 값입니다. true는 기한이 설정되었음을 의미하고, false는 기한이 설정되지 않은 경우 취소 함수를 수동으로 호출하여 컨텍스트를 취소해야 함을 의미합니다.

완료: 읽기 전용 채널을 반환합니다(취소된 후에만 반환됨). 유형은 struct{}입니다. 이 채널을 읽을 수 있으면 상위 컨텍스트가 이 신호를 기반으로 취소 요청을 시작했음을 의미하며 개발자는 일부 정리 작업을 수행하고 고루틴을 종료할 수 있습니다.

Err: 컨텍스트가 취소된 이유를 반환합니다.
: 키-값 쌍인 컨텍스트에 바인딩된 값을 반환하므로 해당 값은 키를 통해 얻을 수 있습니다. 이 값은 일반적으로 스레드로부터 안전합니다.

2. 컨텍스트는 왜 필요한가요?

고루틴이 열리면 강제로 닫을 수 없습니다.

코루틴을 닫는 일반적인 이유는 다음과 같습니다:

    goroutine이 실행을 마치고 자체적으로 종료됩니다.

    주 프로세스가 충돌하고 종료되며 goroutine이 강제로 종료됩니다.

    채널을 통해 신호 보내기 코루틴 종료를 안내합니다.

첫 번째 유형은 정상적인 종료이며 오늘 논의 범위에 포함되지 않습니다.

두 번째 유형은 비정상 종료이므로 코드를 최적화해야 합니다.

세 번째 방법은 개발자가 코루틴을 수동으로 제어할 수 있다는 것입니다. 코드 예시는 다음과 같습니다.

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

}
로그인 후 복사
이 예시에서는 배경 고루틴을 종료하라고 알리는 stop chan을 정의합니다. 구현도 매우 간단합니다. 백그라운드 고루틴에서는 stop이 값을 받을 수 있는지 여부를 결정하는 데 사용합니다. 이는 값을 받지 못하면 종료하고 중지할 수 있음을 의미합니다. , 가 실행됩니다. 기본 모니터링 로직중지 알림이 수신될 때까지 계속 모니터링합니다. 위는 고루틴 시나리오입니다. 여러 개의 고루틴이 있고 각 고루틴 아래에 여러 개의 고루틴 시나리오가 열리면 어떻게 될까요? Context를 사용하는 이유에 대한 Feixue Qingqing의 블로그에서 그는 다음과 같이 말했습니다. chan+select 방법은 고루틴을 종료하는 더 우아한 방법이지만 이 방법에도 제한이 있습니다. 끝을 통제해? 이 고루틴이 다른 고루틴을 생성하면 어떻게 될까요? 끝없는 고루틴 레이어가 있다면 어떨까요? 이는 매우 복잡합니다. 많은 채널을 정의하더라도 고루틴의 관계 체인이 이 시나리오를 매우 복잡하게 만들기 때문에 이 문제를 해결하기 어려울 것입니다.

저는 그가 여기서 말한 내용에 별로 동의하지 않습니다. 왜냐하면 하나의 채널만 사용해도 여러 고루틴을 제어(취소)하는 목적을 달성할 수 있다고 생각하기 때문입니다. 이를 확인하기 위해 예를 들어보겠습니다.

이 예제의 원리는 다음과 같습니다. close를 사용하여 채널을 닫은 후 채널이 버퍼링되지 않으면 원래 차단에서 비차단, 즉 읽기 가능으로 변경되지만 읽기 값은 항상 0입니다. 따라서 이 기능을 기반으로 채널을 소유한 고루틴을 닫아야 하는지 여부를 결정할 수 있습니다.

监控器4,正在监控中...
监控器1,正在监控中...
监控器2,正在监控中...
监控器3,正在监控中...
监控器5,正在监控中...
监控器2,接收到通道值为:false,监控结束。
监控器3,接收到通道值为:false,监控结束。
监控器5,接收到通道值为:false,监控结束。
监控器1,接收到通道值为:false,监控结束。
监控器4,接收到通道值为:false,监控结束。
主程序退出!!
로그인 후 복사

출력은 다음과 같습니다

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

}
로그인 후 복사
위의 예에서는 버퍼링되지 않은 채널을 정의할 때 모든 고루틴을 닫고 싶을 때 close를 사용하여 채널을 닫은 다음 채널이 닫혀 있는지 지속적으로 확인할 수 있음을 보여줍니다. 모든 고루틴(전제조건) 채널만 닫고 다른 데이터를 보내지 않겠다고 합의해야 합니다. 그렇지 않으면 한 번 데이터를 전송하면 고루틴이 닫힙니다. 이는 기대에 미치지 못하므로 다른 레이어를 추가하는 것이 가장 좋습니다. 이 채널에 대한 캡슐화 제한) 고루틴을 종료할지 여부를 결정합니다.

여기서 볼 수 있듯이 저는 초보자로서 Context를 사용해야 할 불가피한 이유를 아직 찾지 못했습니다. Context를 사용하면 몇 가지 문제를 더 쉽게 처리할 수 있다는 점만 말씀드릴 수 있습니다. 동시성을 다루지만 반드시 필요한 것은 아닙니다.

즉,

할 수 있는지🎜의 문제를 해결하는 것이 아니라 🎜사용하기 더 좋은🎜의 문제를 해결하는 것입니다. 🎜🎜🎜3. 간단히 Context를 사용하세요🎜🎜🎜위의 폐쇄 채널 방법을 사용하지 않는 경우 이를 달성할 수 있는 더 우아한 방법이 있습니까? 🎜🎜🎜예, 이것이 Context에 관한 기사입니다.🎜🎜🎜위의 예를 수정하기 위해 Context를 사용했습니다. 🎜
ctx, cancel := context.WithCancel(context.Background())
로그인 후 복사
로그인 후 복사
🎜여기서 키코드는 딱 세줄이에요🎜

第一行:以 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
go
원천:juejin.im
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!