1、为什么需要同步原语?在Go中,多个协程可以同时访问同一份数据。如果同时有多个人给你的银行卡上存钱,一个存1w,一个存2w,第三个存3w。他们同时操作你的余额(balance),就可能出现竞争条件(race condition),三个人都读到旧余额,再各自加钱,结果只加了一次。为了解决这个问题,就需要同步原语来支撑:保护共享资源和协调多个协程的执行顺序与结束时机。2、sync.Mutex互斥锁同一时间,只允许一个协程访问被保护的资源,以多个人给你存钱为例
package main
import (
"fmt"
"sync"
)
var (
balance int
mu sync.Mutex
)
// 存钱-有锁
func cunqianWithLock(amount int, wg *sync.WaitGroup) {
mu.Lock() // 上锁
balance += amount
mu.Unlock() // 解锁
wg.Done() // 本Goroutine任务完成
}
// 存钱-无锁
func cunqianWithNoLock(amount int, wg *sync.WaitGroup) {
balance += amount
wg.Done()
}
func main() {
// 1、无锁版
wg := sync.WaitGroup{}
for i := 0; i < 10000; i++ {
wg.Add(3)
go cunqianWithNoLock(20000, &wg)
go cunqianWithNoLock(30000, &wg)
go cunqianWithNoLock(10000, &wg)
}
wg.Wait()
fmt.Println("1、余额:", balance)
// 2、有锁版
balance = 0
var wg2 sync.WaitGroup
for j := 0; j < 10000; j++ {
wg2.Add(3)
go cunqianWithLock(10000, &wg2)
go cunqianWithLock(20000, &wg2)
go cunqianWithLock(30000, &wg2)
}
wg2.Wait() // 等待所有存款完成
fmt.Println("2、余额:", balance)
}
运行结果:3、sync.WaitGroup等待组等待一组协程全部完成,例如上面的存钱
var wg2 sync.WaitGroup
for j := 0; j < 10000; j++ {
// wg计数器增加3
wg2.Add(3)
// 协程函数里有一个gw.Done(),就是wg计数器减一
go cunqianWithLock(10000, &wg2)
go cunqianWithLock(20000, &wg2)
go cunqianWithLock(30000, &wg2)
}
// 等待所有存款完成,也就是wg的计数器为0的时候
wg2.Wait()
fmt.Println("2、余额:", balance)
4、sync.RWMutex读写锁(多个读同时进行,但是写独占)RLock():读锁;Lock():写锁;多个协程可以同时读同一份数据,但只要有一个在写,其他读写都必须等它写完。
想象有一个“公告栏”:多个人可以同时来看(读锁);但如果有人要改公告(写锁),必须等所有人离开,独自修改;改完后,别人才能再来看。
package main
import (
"fmt"
"sync"
"time"
)
var (
data = 0
rwMu sync.RWMutex
wg sync.WaitGroup
)
func readData(id int) {
defer wg.Done()
rwMu.RLock() // 加读锁
fmt.Printf("读者 %d 读到数据: %d\n", id, data)
time.Sleep(100 * time.Millisecond)
rwMu.RUnlock() // 解读锁
}
func writeData(id int) {
defer wg.Done()
rwMu.Lock() // 加写锁
data++
fmt.Printf("作者 %d 写入: %d\n", id, data)
time.Sleep(200 * time.Millisecond)
rwMu.Unlock() // 解写锁
}
func main() {
for i := 1; i <= 10; i++ {
wg.Add(1)
go readData(i)
}
wg.Add(1)
go writeData(1)
wg.Wait()
}
运行结果:5、sync.Once执行一次(初始化)保证操作只执行一次,常用于单例初始化
想象你家第一次开电视时,系统要“初始化频道设置”。这个操作只需要执行一次;以后再开机,直接用即可,不需要重复设置。
package main
import (
"fmt"
"sync"
)
var once sync.Once
func initConfig() {
fmt.Println("初始化配置中...")
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
once.Do(initConfig) // 无论调用多少次,只执行一次
fmt.Printf("协程 %d 使用配置.\n", id)
}(i)
}
wg.Wait()
}
运行结果:
6、sync.Cond条件变量一种高级的同步机制,用于在特定条件下等待或唤醒协程。主要方法:
- Wait(): 等待条件满足;
- Signal():唤醒一个等待的协程;
- Broadcast():唤醒所有等待的协程;
想象火车站候车室:乘客(协程)提前到达,等待列车(条件 ready);当广播通知“列车进站”时(Broadcast()),所有人被唤醒去上车;如果只叫一个乘客(Signal()),那就是只唤醒一个等待的协程。
package main
import (
"log"
"sync"
"time"
)
var (
cond = sync.NewCond(&sync.Mutex{})
ready = false
)
func worker(id int) {
cond.L.Lock()
for !ready {
cond.Wait() // 等待条件满足
}
log.Printf("工人 %d 开始工作!\n", id)
cond.L.Unlock()
}
func main() {
for i := 1; i <= 10; i++ {
go worker(i)
}
log.Println(`睡2秒`)
time.Sleep(2 * time.Second)
log.Println(`醒了`)
cond.L.Lock()
ready = true
cond.Signal() //唤醒一个等待的协程
// cond.Broadcast() // 唤醒所有等待的协程
cond.L.Unlock()
time.Sleep(1 * time.Second)
}
运行结果:唤醒一个协程
唤醒所有协程
留个作业:多个协程同时读写一个map的内容,会出现什么问题?
人生的并发不是混乱,而是协同;
成长的同步,不是停滞,而是智慧。
让我们像 Go 的并发世界那样:
各自努力,互相守护;
有序前进,终将汇聚成更强大的自己。
源码地址
1、公众号“Codee君”回复“源码”获取源码
如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!