sync
本文最后更新于:2023年12月5日 晚上
读锁:读读不互斥。加了读锁,其他协程仍然可以读,但是不能写
写锁:独占。加了写锁,其他协程无论读写,都不被允许
sync包我们一般不用,推荐使用channel,但是我们也要熟悉。
互斥锁和自旋锁
互斥锁
共享资源的使用是互斥的,即一个线程获得资源的使用权后就会将该资源加锁,使用完后会将其解锁,如果在使用过程中有其他线程想要获取该资源的锁,那么它就会被阻塞陷入睡眠状态,直到该资源被解锁才会被唤醒,如果被阻塞的资源不止一个,那么它们都会被唤醒,但是获得资源使用权的是第一个被唤醒的线程,其它线程又陷入沉睡。
互斥锁在各个语言中定义都不太相同,在大多数语言中,互斥锁使用线程调度来实现的,假如现在锁被锁住了,那么后面的线程就会进入”休眠”状态,直到解锁之后,又会唤醒线程继续执行。这也叫空等待(sleep-waiting)。
但严格来说,只要是同一时刻只能被拿到一次的锁都叫互斥锁,自旋锁也是
在golang中, sync.Mutex就是一个开箱即用的互斥锁,它能保证在同一时刻只有一个”协程”能拿到锁,golang中就同时使用了”自旋”和”休眠”两种方式来实现互斥锁。
自旋锁
自旋锁也是广义上的互斥锁,是互斥锁的实现方式之一,它不会产生线程的调度,而是通过”循环”来尝试获取锁,优点是能很快的获取锁,缺点是会占用过多的CPU时间,这被称为忙等待(busy-waiting)。
interface Locker
Locker接口代表一个可以加锁和解锁的对象。
type Once
Once是只执行一次动作的对象。
func (o *Once) Do(f func())
Do方法当且仅当第一次被调用时才执行函数f。换句话说,给定变量: var once sync.Once
, 如果once.Do(f)
被多次调用,只有第一次调用会执行f
,即使f
每次调用Do
提供的f
值不同。
Do用于必须刚好运行一次的初始化。因为f
是没有参数的,因此可能需要使用闭包来提供给Do方法调用:
因为只有f
返回后Do方法才会返回,f
若引起了Do的调用,会导致死锁。
type Mutex
Mutex是一个互斥锁,又叫拍他锁或者写锁,零值为解锁状态。Mutex类型的锁和线程无关,可以由不同的goroutine加锁和解锁。
如果想要区分读、写锁,可以使用sync.RWMutex类型,见后文。
在Lock()和Unlock()之间的代码段称为资源的临界区(critical section),在这一区间内的代码是严格被Lock()保护的,是线程安全的,任何一个时间点都只能有一个goroutine执行这段区间的代码。
func (m *Mutex) Lock()
声明互斥锁m:var m sync.Mutex
m.Lock()
对m加互斥锁,如果再次执行m.Lock()
,则会阻塞,直到m.Unlock()
对m解锁,第二次对m加锁才会生效。以此实现对数据的保护。
func (m *Mutex) Unlock()
声明互斥锁m:var m sync.Mutex
m.Unlock()
对m解锁,如果m未加锁会导致运行时错误:panic: sync: unlock of unlocked mutex
type RWMutex
读锁:又叫共享锁,可以并发读
RWMutex是读写锁。 该锁可以加多个读锁或者一个写锁,其经常用于读次数远远多于写次数的场景;零值为解锁状态。RWMutex类型的锁也和线程无关,可以由不同的goroutine加读取锁/写入和解读取锁/写入锁。
RWMutex是基于Mutex的,在Mutex的基础之上增加了读、写的信号量,并使用了类似引用计数的读锁数量。
声明一个读写锁:var rw sync.RWMutex
- rw 可以同时加多个读锁
- rw 加读锁时再加写锁将阻塞,有写锁时再加读锁将阻塞
- rw 加写锁,后续再加读锁和写锁都将阻塞
func (rw *RWMutex) Lock()
对 rw 加写锁,后续再加读锁和写锁都将阻塞
func (rw *RWMutex) Unlock()
对 rw 解写锁
func (rw *RWMutex) RLock()
对 rw 加读锁,在执行RUnlock解读锁之前,后续可以再加读锁,但是加写锁会阻塞
func (rw *RWMutex) RUnlock()
对 rw 解读锁
func (rw *RWMutex) RLocker() Locker
返回一个实现了Lock()和Unlock()方法的Locker接口
type Cond
参考:
https://studygolang.com/articles/28072
https://segmentfault.com/a/1190000038319116
https://zhuanlan.zhihu.com/p/351776260
func NewCond(l Locker)
func (c *Cond) Broadcast()
func (c *Cond) Signal()
func (c *Cond) Wait()
type Map
Go中的map不是并发安全的,在Go1.9之后,引入了sync.Map
:并发安全的map
参考:
https://www.cnblogs.com/ricklz/p/13659397.html
http://c.biancheng.net/view/34.html
func (m *Map) Delete(key interface{})
func (m *Map) Load(key interface{}) (value interface{}, ok bool)
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool)
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
func (m *Map) Range(f func(key, value interface{}) bool)
func (m *Map) Store(key, value interface{})
type Pool
pool:资源池
参考:https://www.jianshu.com/p/8fbbf6c012b2
- 临时对象
- 自动移除
- 当这个对象的引用只有sync.Pool持有时,这个对象内存会被释放
- 多线程安全
- 目的就是缓存并重用对象,减少GC的压力
- 自动扩容、缩容
- 不要去拷贝pool,也就是说最好单例
Pool的合理用法是用于管理一组静静的被多个独立并发线程共享并可能重用的临时item。
func (p *Pool) Get
Get方法从池中选择任意一个item,删除其在池中的引用计数,并提供给调用者。Get方法也可能选择无视内存池,将其当作空的。调用者不应认为Get的返回这和传递给Put的值之间有任何关系。
假使Get方法没有取得item:如p.New非nil,Get返回调用p.New的结果;否则返回nil。
func (p *Pool) Put
Put方法将x放入池中。
type WaitGroup
WaitGroup用于等待一组goroutine的结束。父goroutine调用Add方法来设定应等待的goroutine的数量。每个被等待的goroutine在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束。
示例:
注意:
func (wg *WaitGroup) Add
Add方法向内部计数加上delta,delta可以是负数;如果内部计数器变为0,Wait方法阻塞等待的所有goroutine都会释放,如果计数器小于0,方法panic。注意Add调用应在Wait之前,否则Wait可能只会等待很少的goroutine。一般来说本方法应在创建新的goroutine或者其他应等待的事件之前调用。
func (wg *WaitGroup) Done
Done方法减少WaitGroup计数器的值,应在每个等待的goroutine的最后执行。
func (wg *WaitGroup) Wait
Wait方法阻塞直到WaitGroup计数器减为0。