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才可以获取对应的值,这个值一般是线程安全的。当一个协程(goroutine)开启后,我们是无法强制关闭它的。
常见的关闭协程的原因有如下几种:
第一种,属于正常关闭,不在今天讨论范围之内。
第二种,属于异常关闭,应当优化代码。
第三种,才是开发者可以手动控制协程的方法,代码示例如下:
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
里的监控逻辑,继续监控,只到收到stop
deadline
완료
: 읽기 전용 채널을 반환합니다(취소된 후에만 반환됨). 유형은 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("主程序退出!!")
}
여기서 볼 수 있듯이 저는 초보자로서 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,接收到通道值为:{},监控结束。 主程序退出!!
创建 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 }
上面在定义我们自己的 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 在上面已经讲过了,下面就不再举例了
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 主程序退出!!
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 主程序退出!!
通过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 主程序退出!!
推荐教程:《Go教程》
위 내용은 Go 언어의 Context에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!