并发安全

Web 服务器天然是并发的——多个请求同时处理,每个请求可能在不同的 goroutine 中。理解 Gin 的并发安全特性,对编写正确的并发代码至关重要。

Context 不是线程安全的

Gin 的 Context 设计为每个请求一个实例,不是线程安全的:

// 危险:在 goroutine 中直接使用 Context
func handler(c *gin.Context) {
    go func() {
        // 数据竞争!
        c.Set("key", "value")
        c.JSON(200, gin.H{})
    }()
}

正确的并发模式

1. 使用 Context 副本

func handler(c *gin.Context) {
    cCp := c.Copy()
    
    go func() {
        // 安全:使用副本读取数据
        userID := cCp.GetInt("userID")
        processAsync(userID)
    }()
    
    c.JSON(200, gin.H{"status": "accepted"})
}

2. 先提取需要的数据

func handler(c *gin.Context) {
    // 先提取需要的数据
    userID := c.GetInt("userID")
    path := c.Request.URL.Path
    
    go func() {
        // 安全:使用局部变量
        processAsync(userID, path)
    }()
    
    c.JSON(200, gin.H{"status": "accepted"})
}

3. 使用通道传递数据

func handler(c *gin.Context) {
    resultCh := make(chan Result)
    
    go func() {
        result := doWork()
        resultCh <- result
    }()
    
    select {
    case result := <-resultCh:
        c.JSON(200, result)
    case <-time.After(5 * time.Second):
        c.JSON(408, gin.H{"error": "timeout"})
    }
}

全局状态的并发安全

全局变量需要保护

var (
    counter int
    mu      sync.Mutex
)

func handler(c *gin.Context) {
    mu.Lock()
    counter++
    current := counter
    mu.Unlock()
    
    c.JSON(200, gin.H{"count": current})
}

使用 sync.Map

var requestCounts sync.Map

func handler(c *gin.Context) {
    ip := c.ClientIP()
    
    // 原子操作
    count, _ := requestCounts.LoadOrStore(ip, 0)
    requestCounts.Store(ip, count.(int)+1)
    
    c.JSON(200, gin.H{"count": count})
}

使用 atomic 包

var requestCount int64

func handler(c *gin.Context) {
    // 原子增加
    count := atomic.AddInt64(&requestCount, 1)
    
    c.JSON(200, gin.H{"count": count})
}

数据库连接池

数据库连接池本身是线程安全的:

var db *sql.DB

func main() {
    var err error
    db, err = sql.Open("postgres", dsn)
    if err != nil {
        log.Fatal(err)
    }
    
    // 设置连接池参数
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(5)
    
    // db 可以在多个 goroutine 中安全使用
}

func handler(c *gin.Context) {
    // 安全:db 是线程安全的
    var name string
    err := db.QueryRow("SELECT name FROM users WHERE id = $1", 1).Scan(&name)
    // ...
}

缓存的并发安全

使用 sync.RWMutex

type Cache struct {
    data map[string]interface{}
    mu   sync.RWMutex
}

func (c *Cache) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    val, ok := c.data[key]
    return val, ok
}

func (c *Cache) Set(key string, value interface{}) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = value
}

var cache = &Cache{data: make(map[string]interface{})}

func handler(c *gin.Context) {
    // 读锁
    if val, ok := cache.Get("key"); ok {
        c.JSON(200, val)
        return
    }
    
    // 获取数据并缓存
    data := fetchData()
    cache.Set("key", data)
    c.JSON(200, data)
}

常见并发问题

数据竞争

// 错误:数据竞争
var counter int

func handler(c *gin.Context) {
    go func() {
        counter++ // 数据竞争
    }()
    c.JSON(200, gin.H{"count": counter})
}

// 正确:使用 atomic
func handler(c *gin.Context) {
    go func() {
        atomic.AddInt64(&counter, 1)
    }()
    c.JSON(200, gin.H{"count": atomic.LoadInt64(&counter)})
}

死锁

// 错误:死锁
var mu sync.Mutex

func handler(c *gin.Context) {
    mu.Lock()
    defer mu.Unlock()
    
    // 某些条件下再次获取锁
    someFunction() // 如果 someFunction 也获取 mu,会死锁
}

// 正确:避免嵌套锁或使用 RWMutex
var mu sync.RWMutex

func handler(c *gin.Context) {
    mu.RLock()
    defer mu.RUnlock()
    // 读操作
}

Goroutine 泄漏

// 错误:goroutine 泄漏
func handler(c *gin.Context) {
    ch := make(chan int)
    
    go func() {
        ch <- doWork() // 如果没有接收者,goroutine 会一直阻塞
    }()
    
    // 如果请求提前结束,goroutine 泄漏
}

// 正确:使用带缓冲的通道或 context
func handler(c *gin.Context) {
    ch := make(chan int, 1)
    
    go func() {
        ch <- doWork()
    }()
    
    select {
    case result := <-ch:
        c.JSON(200, result)
    case <-c.Request.Context().Done():
        return
    }
}

检测数据竞争

Go 提供了数据竞争检测工具:

go run -race main.go
go test -race ./...

示例输出:

==================
WARNING: DATA RACE
Write at 0x000001234567 by goroutine 8:
  main.handler.func1()
      /app/main.go:20 +0x123

Previous read at 0x000001234567 by goroutine 7:
  main.handler()
      /app/main.go:22 +0x234
==================

小结

Gin 并发安全的关键点:

  • Context 不是线程安全的,不要在 goroutine 中直接使用
  • 使用 Copy() 创建只读副本
  • 先提取数据再传递给 goroutine
  • 全局状态需要适当的同步机制
  • 使用 -race 标志检测数据竞争

理解并发安全是编写可靠 Web 服务的基础。在 Gin 中,遵循"每个请求一个 Context"的原则,正确处理跨 goroutine 的数据共享,就能避免大部分并发问题。