并发冲突案例与解决

并发冲突

package main

import "github.com/gin-gonic/gin"

func main() {
	// 1、创建一个默认的路由引擎(包含日志Logger和恢复Recovery中间件)
	r := gin.Default()

	// 2、定义路由:当访问 /hello 时执行逻辑
	count := 0                             // TODO 存在并发安全问题
	r.GET("/hello", func(c *gin.Context) { // 请求方式 + 请求路径 + 执行逻辑
		count++
		// 3、返回JSON响应,状态码200
		c.JSON(200, gin.H{ // gin.H 底层是map[string]any
			"message": "Hello Go-Learning",
			"from":    "Gin Framework",
			"count":   count,
		})
	})

	// 4、启动服务,默认监听8080端口
	r.Run()

}

count++底层分3步(非原子性)

  • 读count值
  • 加1
  • 写回新值

协程并发访问count,先写入的值会被后写入的值覆盖。
例子:
Mac终端轻量压测工具ab并发100协程,发送10000请求访问http://localhost:8080/hello
ab -n 10000 -c 100 http://localhost:8080/hello
若无并发冲突,再次访问该接口时响应体中count应为10001

# 方式1:浏览器地址栏直接访问curl http://localhost:8080/hello
# 方式2 借助curl命令
curl http://localhost:8080/hello

浏览器
image
终端
image
该接口的访问计数器count数值出现混乱,不是从10001开始,发生了并发冲突。

解决

在 Go 语言中,处理这种简单的数值并发冲突,通常有以下两种方案

方案1:使用原子操作 (sync/atomic)

对于像计数器这种简单的整数加减,Go 提供了底层硬件支持的原子操作。它不需要复杂的上下文切换,性能是最高的。

  • 1、将count定义为int64类型(原子操作库的要求)
  • 2、使用 atomic.AddInt64(&count, 1) 实现自增
  • 3、使用 atomic.LoadInt64 安全地读取当前数值
import (
	"sync/atomic" // 引入原子操作包
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	
	var count int64 = 0 // 定义为 int64 类型的原子变量

	r.GET("/hello", func(c *gin.Context) {
		// 原子自增 1,该操作在 CPU 层面是不可分割的
		newVal := atomic.AddInt64(&count, 1)

		c.JSON(200, gin.H{
			"message": "Hello Go-Learning",
			"count":   newVal, // 直接使用自增后的返回值
		})
	})
	r.Run()
}

image

方案2:使用互斥锁 (sync/Mutex)

互斥锁(Mutex)通过“排队”机制确保同一时刻只有一个协程能访问临界区。虽然性能略逊于原子操作,但它更通用,可以保护更复杂的逻辑(比如同时修改多个变量)。

  • 1、定义一个sync.Mutex实例。
  • 2、读取和修改变量前调用Lock()。
  • 3、操作完成后及时调用Unlock()。
package main

import (
	"github.com/gin-gonic/gin"
	"sync"
)

func main() {
	r := gin.Default()

	count := 0                             // 全局计数器
	var mu sync.Mutex                      // 1、声明一把互斥锁
	r.GET("/hello", func(c *gin.Context) { // 请求方式 + 请求路径 + 执行逻辑
		// 2、加锁,进入临界区
		mu.Lock()

		count++
		currentCount := count // 3、将计算后的值暂存到局部变量中

		// 4、离开临界区,立即解锁
		mu.Unlock()

		// 注意:尽量不要把 IO 操作(如 c.JSON 网络传输)包裹在锁内部,会严重拖慢并发性能
		c.JSON(200, gin.H{
			"message": "Hello Go-Learning",
			"from":    "Gin Framework",
			"count":   currentCount,
		})
	})

	r.Run(":8080")
}

image
image
接口P99从87ms升至141ms,互斥锁带来一定性能损耗。

posted @ 2026-04-06 17:15  Nickey103  阅读(1)  评论(0)    收藏  举报