会进行累加,每次加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 当锁用使用。