Go高级教程:其他并发工具

Go 高级教程:其他并发工具 如果说 Goroutine 和 Channel 是 Go 并发的“常规武器”,那么 sync 包里的工具就是“特种装备”。虽然不常用,但关键时刻能救命(榨干 CPU 的最后一点性能)。 除了 这里 介绍的诸多基础并发工具外,Go 标准库还提供了一些高级并发工具,下面介绍几个比较常用的。 1. 减轻 GC 压力:sync.Pool 我们在讲 GC 的时候提过,如果你频繁申请和销毁大对象(比如 HTTP Response 对象,或者大的 byte buffer),GC 会鸭梨山大。 sync.Pool 就是为了对象复用而生的。 1.1 示例代码 1package main 2 3import ( 4 "fmt" 5 "sync" 6) 7 8// 定义池子 9var bufPool = sync.Pool{ 10 // New 函数:当池子里没存货时,调用它创建一个新的 11 New: func() interface{} { 12 fmt.Println("Creating new buffer") 13 return make([]byte, 1024) 14 }, 15} 16 17func main() { 18 // 1.Get(): 借一个对象 19 buf := bufPool.Get().([]byte) 20 21 // 用完它... 22 23 // 2. Put(): 还回去,下次给别人用 24 // 注意:还之前最好重置一下状态(比如清空) 25 bufPool.Put(buf) 26 27 // 再次 Get,就不会触发 New,而是直接复用刚才那个 28 buf2 := bufPool.Get().([]byte) 29 _ = buf2 30} 1.2 注意事项 sync.Pool 里的对象随时可能被 GC 回收!所以绝对不要用它存数据库连接、Socket 连接这种必须长久保持的资源。它只适合存“临时垃圾”。 ...

2025-12-20 · 3 min · 475 words · Hank

GoLang教程——Context上下文实战

在 Go 的并发编程中,context(上下文)包是绝对的核心。无论是处理 HTTP 请求、RPC 调用,还是数据库查询,Context 都扮演着控制 Goroutine 生命周期的角色。 简而言之,Context 主要解决三个问题: 取消信号:通知子 Goroutine 停止工作,释放资源。 超时控制:规定任务必须在多长时间内完成,否则强制取消。 数据传递:在调用链中传递请求范围内的元数据(如 UserID, TraceID)。 1. 为什么需要 Context? 假设你启动了一个 Goroutine 去查询数据库,如果用户突然关闭了浏览器,或者请求处理太慢超时了,你希望这個后台查询任务能立即停止,而不是继续浪费数据库资源。这就是 Context 的用武之地。 Go 的设计原则:谁启动了 Goroutine,谁就有责任(通过 Context)管理它的退出。 2. 超时控制 (WithTimeout) 这是 Context 最常用的场景。 1package main 2 3import ( 4 "context" 5 "fmt" 6 "time" 7) 8 9func main() { 10 // 1. 创建一个带超时的 Context 11 // 规定任务必须在 2 秒内完成,否则 ctx 会收到取消信号 12 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 13 // 关键:函数结束时必须调用 cancel,防止内存泄漏 14 defer cancel() 15 16 fmt.Println("Start working...") 17 18 // 2. 将 ctx 传递给耗时任务 19 doSlowWork(ctx) 20} 21 22func doSlowWork(ctx context.Context) { 23 // 模拟一个需要 3 秒才能完成的任务 24 // Select 会等待 done 信号或者 work 完成,以此来实现超时抢占 25 select { 26 case <-time.After(3 * time.Second): // 模拟业务逻辑 27 fmt.Println("Work done successfully!") 28 case <-ctx.Done(): // 监听 Context 的取消信号 29 // 如果超时了,ctx.Err() 会返回 DeadlineExceeded 30 fmt.Println("Work cancelled:", ctx.Err()) 31 } 32} 运行结果: 由于任务耗时 3秒,但 Context 限制 2秒,所以输出: Work cancelled: context deadline exceeded ...

2025-03-02 · 2 min · 359 words · Hank

GoLang教程——并发进阶

在掌握了 Goroutine 和 Channel 的基础知识后,我们需要了解 Go 标准库 sync 包提供的各种同步工具。这些工具能帮助我们更好地控制并发程序的执行流程,避免数据竞争,提高程序的可靠性。 sync.WaitGroup:等待一组 Goroutine 完成 基本用法 WaitGroup 用于等待一组 Goroutine 完成。它有三个方法: Add(delta int):增加计数器 Done():减少计数器(相当于 Add(-1)) Wait():阻塞直到计数器为 0 1package main 2 3import ( 4 "fmt" 5 "sync" 6 "time" 7) 8 9func worker(id int, wg *sync.WaitGroup) { 10 defer wg.Done() // 函数结束时调用 Done() 11 12 fmt.Printf("Worker %d starting\n", id) 13 time.Sleep(time.Second) 14 fmt.Printf("Worker %d done\n", id) 15} 16 17func main() { 18 var wg sync.WaitGroup 19 20 // 启动 5 个 worker 21 for i := 1; i <= 5; i++ { 22 wg.Add(1) // 每启动一个 goroutine,计数器 +1 23 go worker(i, &wg) 24 } 25 26 wg.Wait() // 等待所有 worker 完成 27 fmt.Println("All workers completed") 28} 注意事项 Add() 必须在 Wait() 之前调用 Add() 通常在启动 goroutine 之前调用,而不是在 goroutine 内部 必须传递 WaitGroup 的指针,而不是值拷贝 sync.Mutex:互斥锁 基本用法 Mutex(互斥锁)用于保护共享资源,确保同一时间只有一个 goroutine 可以访问。 ...

2024-12-05 · 5 min · 898 words · Hank

GoLang教程——并发基础

Go 语言之所以在云原生时代大放异彩,很大程度上归功于其原生支持的高效并发模型。Go 采用了 Goroutine(协程)和 Channel(通道)来实现并发,这种模式被称为 CSP(通信顺序进程)。本章将带你入门 Go 的并发世界。 示例代码 1package main 2 3import ( 4 "fmt" 5 "time" 6) 7 8// 一个模拟耗时任务的函数 9func worker(id int, jobs <-chan int, results chan<- int) { 10 for j := range jobs { 11 fmt.Printf("Worker %d started job %d\n", id, j) 12 time.Sleep(time.Second) // 模拟耗时 1 秒 13 fmt.Printf("Worker %d finished job %d\n", id, j) 14 results <- j * 2 // 将结果发送回 results 通道 15 } 16} 17 18func main() { 19 // 创建两个通道:任务通道和结果通道 20 // 设置缓冲区大小为 10,防止阻塞 21 jobs := make(chan int, 10) 22 results := make(chan int, 10) 23 24 // 启动 3 个 Goroutine (Worker) 25 // 它们会并发地从 jobs 通道抢任务做 26 for w := 1; w <= 3; w++ { 27 go worker(w, jobs, results) 28 } 29 30 // 发送 5 个任务 31 for j := 1; j <= 5; j++ { 32 jobs <- j 33 } 34 close(jobs) // 关闭任务通道,告知 Worker 没有新任务了 35 36 // 接收并打印结果 37 for a := 1; a <= 5; a++ { 38 <-results 39 } 40 41 fmt.Println("All jobs finished!") 42} 关键点解释 Goroutine (协程) 使用 go 关键字 即可启动一个新的协程。例如 go funcName()。 协程比线程更轻量,启动成本极低,Go 运行时可以在少数几个 OS 线程上调度成千上万个 Goroutine。 main 函数本身也是一个 Goroutine。当 main 结束时,所有其他 Goroutine 都会被强制终止。 Channel (通道) 通道是 Goroutine 之间通信的管道。遵循“不要通过共享内存来通信,而要通过通信来共享内存”的原则。 定义:make(chan Type, capacity)。 发送:ch <- value。 接收:value := <-ch。 关闭:close(ch)。关闭后不能再发送,但可以继续接收已有的数据。 Range:for x := range ch 可以不断从通道读取数据,直到通道被关闭。 Select 选择器 select 语句是 Go 并发编程中的重要工具,用于同时等待多个通道操作。它的语法类似于 switch,但专门用于通道。 ...

2024-11-19 · 3 min · 428 words · Hank

Golang中检测 Data Race

Data Race概念 Data Race (数据竞争),是并发系统中最常见和最难调试的错误类型之一,当两个线程(协程)同时访问同一个变量并且至少其中一次访问是写入时,就会发生 Data Race 。Java 内存模型(JMM)定义了明确的 Happens-Before 规则和提供 Volatile 机制来初步保证程序的执行顺序,Go内存模型(GMM)同样也有相同的规则定义,此外 Golang 还提供了一种检测 Data Race 的机制,帮助开发者更好的分析并发代码的正确性。 go在1.1版本时支持data race 检测。 Data Race 和 Race Condition 的区别: 这两个概念非常容易混淆,stackoverflow上也有讨论这个问题,其实我认为这篇文章很好的区别了它们,所以我进行了翻译。 检测 Data Race 使用 -race 参数可以用来检测这种竞态: 1$ go test -race mypkg // to test the package 2$ go run -race mysrc.go // to run the source file 3$ go build -race mycmd // to build the command 4$ go install -race mypkg // to install the package 1func main() { 2 c := make(chan int) 3 m := make(map[string]int) 4 go func() { 5 m["a"] = 1 // 访问map冲突 6 c <- 1 7 }() 8 m["a"] = 2 // 访问map冲突 9 <-c 10 for k, v := range m { 11 fmt.Printf("key = %v, val = %v\n", k, v) 12 } 13} 示例程序中,两个 goroutine 同时读写 map 存在竞态,可能造成数据不正确的情况,但是错误难以发现,通过执行时添加 -race 选择可以检测: ...

2024-06-20 · 5 min · 980 words · Hank

Data Race vs Race Condition

原文地址: https://blog.regehr.org/archives/490, 翻译并略作改动。 竞态条件(race Condition)是当事件的时间或顺序影响程序的正确性时发生的缺陷。一般来说,需要某种外部计时或排序非确定性来产生竞态条件;典型的例子有上下文切换、操作系统信号、多处理器上的内存操作和硬件 中断。 当程序中有两次内存访问时,就会发生数据竞争(Data Race): 目标为同一内存位置 由两个线程同时执行 不是读取操作 不是同步操作 上边这个定义来自微软研究院的 Sebastian Burckhardt。该定义的两个方面需要注意: “同时”意味着没有像锁这样的东西强制一个操作在另一个操作之前或之后发生。 “不是同步操作”是指程序可能包含特殊的内存操作,例如用于实现锁的操作,这些操作本身并不同步。 在实践中,它们两者存在相当大的重合:许多 Race Condition 是由 Data Race 引起的,并且许多 Data Race 导致 Race Condition。另一方面,两者也可以相互独立,可能产生没有 Data Race 的 Race Condition,也可能产生没有 Race Condition 的 Data Race。 让我们从一个在两个银行账户之间转移资金的简单函数开始: transfer1 (amount, account_from, account_to) { if (account_from.balance < amount) return NOPE; account_to.balance += amount; account_from.balance -= amount; return YEP; } 当然,这并不是银行真正转移资金的方式,但这个例子非常有用。我们知道,账户余额应该是非负的,并且转移之后不能凭空创造(多出)或损失(丢失)金钱。当在没有外部同步的情况下从多个线程调用时,该函数会产生 Data Race(多个线程可以同时尝试更新帐户余额)和 Race Condition(在并行上下文中它将创造或损失金钱)。 我们可以尝试这样修复它: transfer2 (amount, account_from, account_to) { atomic { bal = account_from.balance; } if (bal < amount) return NOPE; atomic { account_to.balance += amount; } atomic { account_from.balance -= amount; } return YEP; } 这里的“atomic”(原子性)是由语言运行时实现的,也许简单地通过在原子块开始时获取线程互斥体(Mutex)并在结束时释放它,也许使用某种事务(Transaction),或者也许通过禁用中断 —— 出于示例的目的,只要 atomic 块内的代码以原子方式执行就能解决竞争问题。 ...

2024-06-18 · 2 min · 235 words · Hank