27·并发编程进阶

Context 与取消传播

contextcanceltimeoutdeadline

第27课:Context 与取消传播

学习目标

  1. 理解 Go 语言中 context 包的核心作用:管理 goroutine 的生命周期、传递取消信号和截止时间。
  2. 掌握创建和使用 Context 的几种基本方式(Background, WithCancel, WithTimeout, WithDeadline)。
  3. 学会在一组相关的 goroutine 中传播取消信号,实现优雅退出。
  4. 了解如何在 Context 中存储和传递请求范围的元数据。

核心概念

想象你正在指挥一场音乐会,你给每位乐手(goroutine)发了一份乐谱。现在,你突然需要立即停止演奏。一种方式是跑去一个个告诉乐手“停下!”,这很慢且不优雅。更好的方式是拥有一个中央广播系统Context)。你只需按下一个按钮(调用 cancel()),所有正在听广播的乐手都会同时收到停止信号。

在 Go 的并发编程中,context 包就是这个“中央广播系统”。它有两个核心任务:

  1. 控制生命周期:通过 Done() 通道发送关闭信号,通知所有正在运行的 goroutine 停止工作并清理资源。
  2. 传递请求范围的值:比如请求 ID、用户认证信息等,这些值可以沿着调用链在不同的 goroutine 间传递。

Context 是一个只读的接口(context.Context),通常作为函数的第一个参数显式传递,其命名约定为 ctx

代码示例

示例1:使用 WithCancel 进行手动取消

package main

import (
	"context"
	"fmt"
	"time"
)

// worker 是一个执行具体任务的函数,它会监听 ctx 的取消信号
func worker(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done(): // 监听取消信号
			fmt.Printf("%s 收到取消信号,原因: %v\n", name, ctx.Err())
			return
		default:
			fmt.Printf("%s 正在工作中...\n", name)
			time.Sleep(500 * time.Millisecond)
		}
	}
}

func main() {
	// 创建一个带有取消功能的 context
	ctx, cancel := context.WithCancel(context.Background())

	go worker(ctx, "Worker-A")
	go worker(ctx, "Worker-B")

	time.Sleep(2 * time.Second) // 让 worker 工作2秒

	fmt.Println("主程序发出取消信号")
	cancel() // 发送取消信号,所有使用此 ctx 的 goroutine 都会收到

	time.Sleep(1 * time.Second) // 等待 goroutine 打印退出信息
	fmt.Println("程序结束")
}

输出示例:

Worker-A 正在工作中...
Worker-B 正在工作中...
... (中间会有多次输出) ...
主程序发出取消信号
Worker-A 收到取消信号,原因: context canceled
Worker-B 收到取消信号,原因: context canceled
程序结束

示例2:使用 WithTimeout 设置超时

package main

import (
	"context"
	"fmt"
	"time"
)

func slowOperation(ctx context.Context) error {
	// 模拟一个需要长时间完成的任务
	time.Sleep(3 * time.Second)
	return nil
}

func main() {
	// 创建一个1秒后自动取消的 context
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel() // 在函数退出前调用 cancel 是一个好习惯,即使超时也会触发

	err := slowOperation(ctx)
	if err != nil {
		fmt.Println("操作出错:", err)
	}

	// 检查 context 是否因超时而取消
	if ctx.Err() == context.DeadlineExceeded {
		fmt.Println("操作因超时而被取消")
	} else {
		fmt.Println("操作完成")
	}
}

输出:

操作因超时而被取消

示例3:在 HTTP 服务器中使用 Context

这是 context 最常见的应用场景之一。

package main

import (
	"context"
	"fmt"
	"net/http"
	"time"
)

func handler(w http.ResponseWriter, r *http.Request) {
	// 从请求中获取 context,它会在客户端断开时自动取消
	ctx := r.Context()
	fmt.Println("handler 开始")

	// 模拟一个耗时的操作,比如数据库查询
	select {
	case <-time.After(3 * time.Second):
		// 操作完成
		fmt.Fprintln(w, "请求处理成功")
	case <-ctx.Done():
		// 客户端提前断开连接
		err := ctx.Err()
		fmt.Println("handler 因客户端取消而中断:", err)
		http.Error(w, "请求被取消", http.StatusRequestTimeout)
	}

	fmt.Println("handler 结束")
}

func main() {
	http.HandleFunc("/", handler)
	fmt.Println("服务器启动于 :8080")
	http.ListenAndServe(":8080", nil)
}

测试方法:启动服务器后,使用 curl --max-time 2 http://localhost:8080 命令,2秒后会因客户端超时而中断。

实践练习

练习1:基本的 Context 取消

要求:创建一个 WithCancel context,并启动3个 goroutine。每个 goroutine 模拟不同的工作(打印不同的计数)。在主函数中,等待1秒后调用 cancel(),确保所有 goroutine 都能优雅退出并打印退出信息。 预期输出:三个 goroutine 打印若干次计数后,都打印出 “收到取消信号,退出”。

练习2:实现一个带超时的缓存查询

要求:编写一个函数 getFromCache(ctx context.Context, key string) (string, error)。该函数模拟从缓存中查询数据,正常需要2秒。使用 WithTimeout 为该操作设置1秒的超时。如果超时,应返回 context.DeadlineExceeded 错误;如果手动取消,则返回 context.Canceled 错误。 预期输出:调用函数后,1秒内应收到超时错误。

练习3:组合 Context

要求:基于练习2的 getFromCache 函数。现在创建一个“父” context(带3秒超时),然后基于这个父 context 再创建一个“子” context(带1秒超时)。调用 getFromCache 时使用这个子 context。观察并解释当子 context 超时后,父 context 是否也会被取消?为什么? 提示:子 context 的取消会导致父 context 的 Done() 通道关闭吗?

常见错误

  1. 忽略 ctx.Done() 的检查:在 for 循环或 select 语句中,不监听 ctx.Done(),导致 goroutine 无法响应取消信号,造成资源泄漏。
  2. 在 goroutine 内部调用 cancel()cancel 函数应由创建 context 的那个函数(通常是调用链的上层)负责调用,并通过 defer cancel() 确保执行。在子 goroutine 中调用会过早取消整个任务。
  3. 将 Context 存储在结构体中Context 应作为函数参数显式传递(通常是第一个参数),而不是作为结构体字段。这使它的生命周期和范围更清晰。
  4. 传递 nil Context:如果不确定用什么 Context,应传递 context.TODO(),而不是 nil,以避免运行时 panic。

小结

  • Context 的核心作用:协调多个 goroutine 的取消和超时,是 Go 并发控制的“遥控器”。
  • 关键函数
    • Background():创建根 Context。
    • WithCancel(parent):创建可手动取消的子 Context。
    • WithTimeout(parent, timeout):创建指定超时后自动取消的子 Context。
    • WithDeadline(parent, time):创建指定截止时间后自动取消的子 Context。
  • 使用模式
    1. 创建 Context(通常是 Background 或从 *http.Request 获取)。
    2. ctx 作为函数的第一个参数传递。
    3. 在函数内部,通过 select 监听 ctx.Done() 通道来响应取消信号。
    4. 使用 ctx.Err() 来获取取消的原因(CanceledDeadlineExceeded)。
  • 最佳实践:尽早检查 ctx.Done(),使用 defer cancel(),并始终处理 ctx.Err() 返回的错误。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「并发模式:Worker Pool」 以巩固所学知识。