每日一Go-11、Go语言并发同步原语:sync包

37 阅读4分钟

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条件变量一种高级的同步机制,用于在特定条件下等待或唤醒协程。主要方法:

  1. Wait(): 等待条件满足;
  2. Signal():唤醒一个等待的协程;
  3. 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君”回复“源码”获取源码

2、pan.baidu.com/s/1B6pgLWfS…


如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!