在Go语言中,sync包提供了用于处理并发常使用的多种同步原语,以下是sync中核心,也是常用的组件:
- Sync.Mutex 互斥锁,用于保护临界区,防止多个goroutinue访问同一资源。
- Sync.RWMutex 读写锁,允许多个协程读取共享资源,而写独占。
- sync.WaitGroup 等待一组协程执行完成。
- sync.Map 一个并发安全的map,适合map的并发读写场景。 参考
- sync.Pool 对象池,管理重复使用的对象,减少内存分配和垃圾回收压力。参考
- sync.Cond 条件变量,用于在协程之间协调事件发生的顺序。参考
- sync.Once 用于单例对象的创建。参考
Sync.Mutex
互斥锁,用于在并发环境中多个协程修改共享数据,通过Lock获取锁,Unlock释放锁。
package main
import (
"fmt"
"sync"
"time"
)
var (
mu sync.Mutex
count int
)
func main() {
wg := new(sync.WaitGroup)
for i := 1; i <= 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
increment()
}(i)
}
wg.Wait()
fmt.Println("count: ", count)
}
func increment() {
for i := 1; i <= 5; i++ {
mu.Lock()
count++
mu.Unlock()
time.Sleep(100 * time.Millisecond)
}
}
实现原理
sync.Mutex的结构体:
type Mutex struct {
state int32
sema uint32
}
state表示当前锁的状态,sema用于控制锁状态的信号量,当持有该锁的协程释放锁后,通过sema来唤醒阻塞等待获取锁的协程。
互斥锁的两种模式
Mutex有两种模式,正常模式和饥饿模式,
正常模式下,所有阻塞在等待队列中的goroutine会按顺序进行锁获取,当唤醒一个等待队列中的goroutine时,此goroutine并不会直接获取到锁,而是会和新请求锁的goroutine竞争。 通常新请求锁的goroutine更容易获取锁,这是因为新请求锁的goroutine正在占用cpu片执行,大概率可以直接执行到获取到锁的逻辑。
饥饿模式下, 新请求锁的goroutine不会进行锁获取,而是加入到队列尾部阻塞等待获取锁。
饥饿模式的触发条件:
- 当一个goroutine等待锁的时间超过1ms时,互斥锁会切换到饥饿模式.
饥饿模式的取消条件:
- 当获取到锁的这个goroutine是等待锁队列中的最后一个goroutine,互斥锁会切换到正常模式
- 当获取到锁的这个goroutine的等待时间在1ms之内,互斥锁会切换到正常模式
总结
sync.Mutex通过state表示锁的状态,但锁被释放后,通过信号量唤醒阻塞等待获取锁的协程。另外锁有两种模式:正常模式和饥饿模式。
sync.RWMutex
读写锁,是互斥锁的一个改进版,允许多个读者同时访问数据但只允许一个写者,用RLock获取读锁,RUnlock释放读锁;Lock和Unlock分别获取锁和释放锁;
package main
import (
"fmt"
"sync"
"time"
)
var (
mu sync.RWMutex
data = make(map[string]string)
)
func main() {
wg := new(sync.WaitGroup)
for i := 0; i < 4; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
readData(fmt.Sprintf("key-%d", i))
}(i)
}
go func() {
writeData("testKey", "testValue")
}()
wg.Wait()
fmt.Printf("final data: %+v\n", data)
}
func readData(key string) {
mu.RLock()
defer mu.RUnlock()
fmt.Printf("read cache key: %s; value: %s\n", key, data[key])
time.Sleep(100 * time.Millisecond)
}
func writeData(key string, value string) {
mu.Lock()
defer mu.Unlock()
data[key] = value
time.Sleep(100 * time.Millisecond)
}
原理
偷个懒 参考
总结
sync.RWMutex是互斥锁的一个改进版,试用于例如缓存、配置信息等读多写少的场景中。特点是:读写互阻塞,读读不阻塞。
sync.WaitGroup
sync.WaitGroup是go标准库sync提供的同步原语,用于等待一组协程执行完成。他的主要作用是等待所有协程执行完成之后做后续动作,避免主程序过早退出。
源码解读
type WaitGroup struct {
noCopy noCopy //避免复制
state atomic.Uint64 // high 32 bits are counter, low 32 bits are waiter count. 高32位表示counter的数据(及未完成的协程数量),低32位表示等待者(waiter)的数量
sema uint32 //信号量, 用于阻塞 / 唤醒 waiter
}
使用方法
-
Add方法
Add方法的主要作用是管理计数器counter,并在counter为0时,唤醒waiter。
-
Done方法
Done方法主要作用是标记一个协程已经执行完成, 减少counter值。
-
Wait方法
Wait方法用于等待counter的值为0,当counter为0时会被runtime_Semrelease唤醒,执行后续操作。
总结
sync.WaitGroup是通过counter的增减追踪协程的完成状态,通过信号量实现阻塞和唤醒。这样能够保证所有协程完成后,主程序才会向下运行。