Go语言进阶之Context控制并发

Context

Context是Go语言中一个用于传递请求范围的上下文信息的标准库包,其主要用于处理并发操作中请求的生命周期的管理。

协程如何退出

利用协程退出的例子来说明Context的作用,以及没有使用Context,应该如何在没有执行完代码时提前退出协程

go 复制代码
package main

import (
	"fmt"
	"time"
)

func main() {
	// 创建一个用于退出的信号 channel
	exitChan := make(chan struct{})

	// 启动一个协程
	go func() {
		for {
			select {
			case <-exitChan:
				fmt.Println("协程收到退出信号,正在退出...")
				return // 退出协程
			default:
				// 模拟一些工作
				fmt.Println("协程正在执行...")
				time.Sleep(1 * time.Second) // 假装在做事情
			}
		}
	}()

	// 主协程等待一段时间后发送退出信号
	time.Sleep(5 * time.Second)
	close(exitChan) // 发送退出信号

	// 等待一段时间,确保协程能够退出
	time.Sleep(1 * time.Second)
	fmt.Println("主协程结束")
}

这段代码使用了for select循环来中途暂停协程运行

当我们启动了一个处主协程之外的协程时,我们可以通过for select循环来选择停止协程与继续协程

虽然这段代码看上去并不长,并且十分好用,但现实中肯定不止这一个协程,如果想同时让很多个协程停止那么代码将会很长,所以这时就要使用Context了。

Context使用示例

将上面的代码使用Context库进行改造

go 复制代码
package main

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

func main() {
	// 创建一个带取消功能的上下文
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel() // 确保在 main 结束时调用取消

	// 启动一个协程
	go func(ctx context.Context) {
		for {
			select {
			case <-ctx.Done():
				fmt.Println("协程收到退出信号,正在退出...")
				return // 退出协程
			default:
				// 模拟一些工作
				fmt.Println("协程正在执行...")
				time.Sleep(1 * time.Second) // 假装在做事情
			}
		}
	}()

	// 主协程等待一段时间后发送退出信号
	time.Sleep(5 * time.Second)
	cancel() // 发送退出信号

	// 等待一段时间,确保协程能够退出
	time.Sleep(1 * time.Second)
	fmt.Println("主协程结束")
}

在这段代码中使用 context.WithCancel创建一个可取消的上下文,其中cancel函数用于取消上下文。

当cancel函数被调用时,ctx.Done()会接收到结束的信号,并传递给case让进程结束。

Context同时处理多个协程

go 复制代码
package main

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

func main() {
	// 创建一个带取消功能的上下文
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel() // 确保在 main 结束时调用取消

	var wg sync.WaitGroup

	// 启动多个协程
	numGoroutines := 3
	for i := 1; i <= numGoroutines; i++ {
		wg.Add(1) // 增加 WaitGroup 计数
		go func(id int, ctx context.Context) {
			defer wg.Done() // 协程完成时调用 Done
			for {
				select {
				case <-ctx.Done():
					fmt.Printf("协程 %d 收到退出信号,正在退出...\n", id)
					return // 退出协程
				default:
					// 模拟一些工作
					fmt.Printf("协程 %d 正在执行...\n", id)
					time.Sleep(1 * time.Second) // 假装在做事情
				}
			}
		}(i, ctx)
	}

	// 主协程等待一段时间后发送退出信号
	time.Sleep(5 * time.Second)
	cancel() // 发送退出信号

	// 等待所有协程完成
	wg.Wait()
	fmt.Println("所有协程已结束,主协程结束")
}

Context详解

Context接口

Context接口方法主要有4种

go 复制代码
1. Deadline() (deadline time.Time,ok bool)
// 这个方法可以获取设置的截止时间,第一个返回值deadline为截止时间,到了这个时间点,Context会自动发起取消请求,第二个返回值ok表示是否设置了截止时间

2. Done() <-chan struct{}
// 这个方法返回一个只读的通道,当上下文被取消时,这个通道就会被关闭。当方法返回的chan可以读取时,则意味着Context已经发起了取消信号。通过Done方法收到这个信号之后,就可以做清理操作,然后退出协程,释放资源

3. Err() error
// 这个方法返回上下文的错误状态,如果上下文被取消,返回context.Canceled;如果超时返回context.DeadlineExceeded

4. Value(key interface{}) interface{}
// 从上下文中获取与特定键关联的值,Value方法获取该Context上绑定的值,是一个键值对,所以要通过Key才可以获取。

上下文的创建(Context树)

上下文树的基本结构:

根上下文:通常使用context.Background() 或 context.TODO() 作为树的根节点。

子上下文:通过context.WithCancel(), context.WithTimeout(), 或 context.WithDeadline()创建的上下文是根上下文或其他上下文的子上下文。

上下文函数详解

Context主要提供了5种方法来创建新的上下文:

go 复制代码
1. context.Background()
// 返回一个空上下文,通常做根上下文,通常在程序的最顶层使用,它可以作为其他上下文的父上下文

2. context.TODO()
// 当不确定使用哪个上下文时,可以使用TODO(),这个上下文的用途通常在代码开发的过程中,表示你需要稍后处理的上下文。

3. context.WithCancel(parent Context)
// 创建一个可取消的上下文,返回一个新上下文和一个取消函数。调用取消函数会取消这个上下文及其所以子上下文。

4. context.WithTimeout(parent Context,timeout time.Duration)
//创建一个带有超时的上下文。当超时时间达到,自动取消上下文

5. context.WithDeadline(parent Context,deadline time.Time)
// 与WithTimeout类似,但是使用绝对时间来设置截止时间
Context树的传播

在Context树中,父上下文的状态会影响到所有子上下文,当父上下文被取消是,所以的子上下嗯也会自动被取消。

go 复制代码
package main

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

func main() {
    // 创建根上下文
    rootCtx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // 创建子上下文
    childCtx, childCancel := context.WithTimeout(rootCtx, 2*time.Second)
    defer childCancel()

    go func(ctx context.Context) {
        select {
        case <-ctx.Done():
            fmt.Println("子上下文被取消:", ctx.Err())
        }
    }(childCtx)

    // 模拟一些工作
    time.Sleep(1 * time.Second)
    
    // 取消根上下文
    cancel()
    
    // 等待子协程结束
    time.Sleep(1 * time.Second)
}

Context传值

Context在go中的作用不仅可以用于取消协程,还可以传值,通过这个能力Context储存的值可以供其他协程使用,这个方式适合传递请求范围内的共享数据。

context的值是通过context.WithValue函数设置的,其中传递的值是不可变的,并且使用interface{}类型实现

go 复制代码
package main

import (
    "context"
    "fmt"
)

type key string

const userKey key = "user"

func main() {
    // 创建一个背景上下文
    ctx := context.Background()

    // 将值存入上下文
    ctx = context.WithValue(ctx, userKey, "Alice")

    // 在 goroutine 中使用上下文
    go func(ctx context.Context) {
        // 从上下文中获取值
        if user, ok := ctx.Value(userKey).(string); ok {
            fmt.Println("User from context:", user)
        } else {
            fmt.Println("User not found in context")
        }
    }(ctx)

    // 等待 goroutine 完成
    // 在实际应用中,使用 sync.WaitGroup 或其他同步机制
    select {}
}
相关推荐
娅娅梨17 分钟前
C++ 错题本--not found for architecture x86_64 问题
开发语言·c++
汤米粥23 分钟前
小皮PHP连接数据库提示could not find driver
开发语言·php
冰淇淋烤布蕾26 分钟前
EasyExcel使用
java·开发语言·excel
Leo.yuan27 分钟前
数据量大Excel卡顿严重?选对报表工具提高10倍效率
数据库·数据分析·数据可视化·powerbi
拾荒的小海螺32 分钟前
JAVA:探索 EasyExcel 的技术指南
java·开发语言
Runing_WoNiu36 分钟前
MySQL与Oracle对比及区别
数据库·mysql·oracle
马剑威(威哥爱编程)1 小时前
哇喔!20种单例模式的实现与变异总结
java·开发语言·单例模式
天道有情战天下1 小时前
mysql锁机制详解
数据库·mysql
看山还是山,看水还是。1 小时前
Redis 配置
运维·数据库·redis·安全·缓存·测试覆盖率
谷新龙0011 小时前
Redis运行时的10大重要指标
数据库·redis·缓存