面试题答案
一键面试可能导致性能下降的原因
- Context创建和传递开销:在高并发场景下,频繁创建和传递
context
对象会带来一定的性能开销。每次创建context
都需要分配内存,传递时也需要在函数调用栈中传递参数。 - Cancel操作开销:当父
context
取消时,所有依赖它的子context
也会被取消。这个传播过程在高并发下可能导致额外的同步开销,尤其是如果子协程数量众多,取消信号的传递和处理可能成为性能瓶颈。 - 资源竞争:如果多个子协程在处理
context
取消信号时竞争共享资源(如文件描述符、数据库连接等),可能会导致锁争用,从而降低性能。
优化方案及原理
- 减少Context创建开销:尽量复用
context
对象,避免在每个子协程创建时都重新创建context
。可以通过将父context
传递到需要的地方,在子协程中直接使用。 - 优化Cancel操作:使用
context.WithCancelCause
(Go 1.20+),它允许在取消context
时携带原因,这可以更精确地控制取消逻辑,减少不必要的取消传播。如果在低版本Go中,可以自定义结构体封装context
和取消函数,在取消时做更细粒度的控制。 - 减少资源竞争:对于共享资源,使用资源池(如连接池)来管理,减少锁争用。同时,在处理
context
取消时,尽量避免对共享资源进行复杂操作,尽快释放资源。
优化后的代码示例
package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d stopped\n", id)
return
default:
fmt.Printf("Worker %d working\n", id)
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(context.Background())
numWorkers := 1000
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go worker(ctx, i, &wg)
}
time.Sleep(500 * time.Millisecond)
cancel()
wg.Wait()
fmt.Println("All workers stopped")
}
在这个示例中:
- 复用Context:通过
context.WithCancel(context.Background())
创建一个父context
,并传递给所有子协程,避免重复创建。 - 正确处理取消:子协程通过
select
监听ctx.Done()
通道,在接收到取消信号时正确退出。 - 性能优化:减少
context
创建和取消的不必要开销,通过default
分支避免select
阻塞,使程序在高并发场景下能更高效地运行。