承上启下
我们在上篇文章中介绍了Sync,可以通过多种同步原语来处理资源竞争的问题。同样的,今天要介绍的context可以用于并发控制的,除此以外,还有很多其他的方法,我们一起来看看。
开始学习
在Go语言中,context
包提供了一种机制,允许传递请求相关的数据、取消信号、截止时间以及其他请求范围的值跨API边界和进程边界。它是处理并发、超时和取消操作的重要工具。
context的基本概念
context
包的核心是Context
接口,它定义了以下方法:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
以下是这些方法的简要说明:
- Deadline():返回一个代表此上下文应该被取消的时间,即它的截止时间。如果没有设置截止时间,
ok
将会是false
。 - Done():返回一个通道(channel),这个通道在上下文被取消或超时时关闭。当通道关闭时,意味着相关的操作应该停止并且清理资源。
- Err():当
Done()
通道关闭后,返回一个错误,解释上下文被取消的原因。如果上下文没有被取消,返回nil
。 - Value():返回此上下文中与
key
相关联的值,如果没有值与key
相关联,则返回nil
。
创建和衍生context
context
包提供了一些函数来创建和衍生上下文:
- Background():返回一个空的上下文,它是所有上下文的根,通常用于主函数、初始化和测试,并且不应该被取消。
- TODO():返回一个空的上下文,当不清楚应该使用哪个上下文或者它还未来得及被设置时,作为占位符使用。
- WithCancel():返回一个新的上下文和取消函数。调用取消函数会取消该上下文及其所有子上下文。
- WithDeadline():返回一个新的上下文,它在指定的截止时间被取消。
- WithTimeout():返回一个新的上下文,它在经过指定的持续时间后被取消。
- WithValue():返回一个新的上下文,其中包含了键值对。
context的使用原则
- 不要将context存储在结构体中,而是作为第一个参数传递给需要它的函数。
- 即使函数不直接使用context,也应该将其传递给其他需要它的函数。
- 不要传递nil的context,如果没有可用的context,应该使用
context.Background()
。 - 仅在API边界和进程间传递context,而不是在包之间传递。
- context的值应该是请求范围的数据,而不是进程范围的数据。
通过遵循这些原则和正确使用context
包,可以有效地管理Go程序中的并发控制和请求生命周期。
并发控制
在Go语言中,context
包是并发控制的关键工具之一。以下是context
如何实现并发控制的几个关键点:
1. 上下文取消
context
包允许创建可以取消的上下文,这样就可以通知所有依赖于该上下文的goroutine停止它们的工作。
创建可取消的上下文:
ctx, cancel := context.WithCancel(context.Background())
这里,WithCancel
函数返回一个新的上下文ctx
和一个cancel
函数。调用cancel
函数会关闭ctx.Done()
通道,这是一个信号,告诉所有监听该通道的goroutine应该停止它们的工作。
在goroutine中使用上下文:
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
// 上下文被取消,执行清理工作并退出goroutine
return
default:
// 执行工作...
}
}
}(ctx)
在这个goroutine中,通过监听ctx.Done()
通道来检查上下文是否已被取消。如果收到取消信号,goroutine将执行必要的清理工作并退出。
2. 上下文超时
context
包还允许设置超时,在超时后自动取消上下文。
创建带超时的上下文:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
这里,WithTimeout
函数创建一个新的上下文ctx
,它在10秒后自动取消。同样,cancel
函数可以用来提前取消上下文。
3. 上下文值传递
context
可以携带键值对,允许在goroutine之间传递数据。
在上下文中设置值:
ctx := context.WithValue(context.Background(), "key", "value")
在goroutine中获取值:
value := ctx.Value("key")
实现并发控制的步骤:
-
创建上下文:根据需要创建一个基础上下文,并使用
WithCancel
、WithTimeout
或WithValue
来设置特定的属性。 -
传递上下文:将上下文作为参数传递给启动的goroutine或函数。
-
监听取消信号:在goroutine中,使用
select
语句监听ctx.Done()
通道,以检测上下文是否被取消。 -
响应取消:当
ctx.Done()
通道关闭时,执行必要的清理工作,并退出goroutine。 -
主动取消:在适当的时机(例如错误发生或操作完成)调用
cancel
函数来取消上下文。
以下是一个简单的例子,展示了如何使用context
来控制并发:
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d received cancel signal\n", id)
return
default:
fmt.Printf("Worker %d is working...\n", id)
time.Sleep(1 * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
for i := 0; i < 3; i++ {
go worker(ctx, i)
}
// 等待足够长的时间来观察goroutine的行为
time.Sleep(10 * time.Second)
}
在这个例子中,三个工作goroutine启动并执行任务。它们都会监听上下文的取消信号。主函数设置了5秒的超时,之后取消上下文,这会导致所有工作goroutine在打印一条消息后退出。