多个 goroutine 修改数据出现竞争的两种解决办法

会进行累加,每次加1

package main
import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func testAdd(a *int) {
	*a = *a + 1
}
func main() {
	num := 0
	testAdd(&num)
	fmt.Println(num)
	testAdd(&num)
	fmt.Println(num)
	testAdd(&num)
	fmt.Println(num)
}

控制台输出:
1
2
3
会进行累加,每次加1,因为:

func testAdd(a *int) {
	*a = *a + 1   // 顺着地址,读到 num 当前的值,+1,再写回同一个格子
}

&num 每次传的都是同一个地址,所以*a 每次都指向同一个num :
累加的不是函数,函数本身没有记忆;
累加的是num 这个变量,它一直在原地被*a 改写。
所以会出现累加的情况。

跑的不是1+2+…+1000, 是每个 goroutine 只加 1

package main
import (
	"fmt"
	"sync"
)
var wg sync.WaitGroup
func add(a *int) {
	*a = *a + 1
	wg.Done()
}
func main() {
	wg.Add(1000)
	num := 0
	// 这里并发了1000个goroutine,每个goroutine都对num进行了加1操作,最后打印出num的值
	for i := 0; i < 1000; i++ {
		go add(&num)
	}
	wg.Wait()
	fmt.Println(num)
}

输出的值:973,不会是1000。
有的小伙伴说:为啥才900+多,1+2+3+1000不应该才这一点值呢?
其实是因为:理解错了。这里跑的不是「1+2+…+1000」,是每个 goroutine 只加 1 。
这里go add(&num)传递是 num 不是i ,
如果是 i 的话,那么执行的就是1+2+…+1000。

多个 goroutine 修改数据会出现竞争

多个 goroutine 修改数据会出现竞争,
会导致输出的值不是1000。

互斥锁来处理goroutine的竞争问题

互斥锁是传统并发编程中对共享资源进行访问控制的主要手段,
sync.Mutex类型只有两个公开的指针方法,
Lock和Unlock。Lock锁定当前的共享资源,Unlock进行解锁。
互斥锁的本质是当一个goroutine访问的时候,其他goroutine都不能访问。
这样资源同步,避免竞争的同时也降低了程序的并发性能。
程序由原来的并行执行变成了串行执行。

package main

import (
	"fmt"
	"sync"
)
var wg sync.WaitGroup
// 声明一个互斥锁,用于保护 count 变量的访问
var mutex sync.Mutex
func add(a *int) {
	// 加锁
	mutex.Lock()
	*a = *a + 1
	// 解锁
	mutex.Unlock()
	wg.Done()
}
func main() {
	wg.Add(1000)
	num := 0
	// 这里并发了1000个goroutine,每个goroutine都对num进行了加1操作,最后打印出num的值
	for i := 0; i < 1000; i++ {
		go add(&num)
	}
	wg.Wait()
	fmt.Println(num)
}

通过通道来解决多个 goroutine 修改数据竞争问题

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func add(a *int, ch *chan struct{}) {
	// 加锁;struct{}是一个类型,struct{}{}是一个值,空值不会占用内存空间
	*ch <- struct{}{}
	*a = *a + 1
	// 解锁
	<-*ch
	wg.Done()
}
func main() {
	wg.Add(1000)
	ch := make(chan struct{}, 1)
	num := 0
	// 这里并发了1000个 goroutine ,每个goroutine都对num进行了加1操作,最后打印出num的值
	for i := 0; i < 1000; i++ {
		go add(&num, &ch)
	}
	wg.Wait()
	fmt.Println(num)
}

能够解决的核心原因是:
cap=1 的 channel 本质上就是一个二元信号量,也就是信号量版的互斥锁 。
如果写成make(chan struct{}, 2) ,两个令牌同时发出,互斥立刻失效。
因为缓冲容量只有 1,意味着“令牌”最多存在一个:

  • 某个 goroutine 发送成功后,它持有令牌,缓冲池已满;
  • 其余 goroutine 的发送操作都会阻塞在channel 的发送队列上,由 runtime 挂起;
  • 只有持令牌的goroutine 执行<-*ch 取走令牌,缓冲区才会空出来,
    runtime 会把发送队列队首那个 goroutine 的值直接填进空出的槽位,并唤醒(goready)它。
    被阻塞的 goroutine 不会重新执行一次发送语句,这次发送由 runtime 代它完成。
    所以同一时刻最多只有一个 goroutine 能执行*a = *a + 1 操作。

优化点: ch 没有必要传递 &ch

根据昨天学的知识点; ch 没有必要传递 &ch;这样还会多一层寻址;
管道channel的值传递就已经共享同一个通道 ,不需要再取地址。

go add(&num, &ch) // 不好的写法

优化为下面的形式

go add(&num, ch)

func add(a *int, ch chan struct{}) {
	// 加锁;struct{}是一个类型,struct{}{}是一个值,空值不会占用内存空间
	ch <- struct{}{}
	*a = *a + 1
	// 解锁
	<-ch
	wg.Done()
}

总结

在实际的开发中,推荐使用互斥锁来解决多个 goroutine 修改数据竞争问题的问题。
别把 channel 当锁用使用。

posted @ 2026-11-11 21:40  何人陪我共长生  阅读(11)  评论(0)    收藏  举报