Go中的协程同步
由于go天然对并发的支持使得go在一些并发任务处理时表现优异,但是并发会存在临界资源问题,当多个协程来访问共享资源时资源是不安全的
在一般场景中为了解决这个临界资源问题,可以使用channel通道来解决,但是当协程结构比较复杂时处理比较复杂易出错
Go的官方sync包实现了传统编程语言中的“锁”,通过一个“锁”将共享资源锁住,防止其它协程的访问,以此来解决临界资源问题
type Once
表示只执行一次动作的对象
func (o *Once) Do(f func()) Do方法当且仅当第一次被调用时才执行指定函数,即使每次调用提供的f值不同 f没有参数和返回值 只有f返回后Do方法才会返回,如果f中引起了Do的调用,会导致死锁 该类型用于系统的一次性初始化相关工作 使用示例 var once sync.Once // 无需初始化 func MyFunc(){ // somecode } // 启用3个协程运行 go once.Do(MyFunc) go once.Do(MyFunc) go once.Do(MyFunc) // 在主线程3次运行 once.Do(MyFunc) once.Do(MyFunc) once.Do(MyFunc) 以上代码片段中,无论是在多个协程中多次运行,还是在主线程中多次运行,可以保证MyFunc函数仅运行一次
func OnceFunc和func OnceValue[s]
用于创建一个可安全用于并发调用的一次性函数,无论多少个协程调用,都仅会在首次调用时执行,后续调用将无任何作用
func OnceFunc(f func()) func() :用于创建没有返回值的一次性函数 func OnceValue[T any](f func() T) func() T :创建一个返回值T的一次性函数 func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) :创建返回两个值的一次性函数 sync.OnceValue[string](func() string { return time.Now().Format("20060102150405") })
type Mutex
表示一个互斥锁,创建为其他结构体的字段用来标注整个结构体实例对象的读写状态
零值为解锁状态,可以由不同的线程加锁和解锁
当一个互斥锁被一个协程加了锁之后其他协程对该互斥锁进行加锁操作都会阻塞直到该互斥锁被解锁
Mutex的方法
Lock() :加锁,如果已经是加锁状态,会阻塞直到该互斥锁解锁 TryLock() bool :尝试加锁并返回是否成功 Unlock():解锁,如果未加锁会导致运行时错误
type RWMutex
表示一个读写互斥锁,创建为其他结构体的字段用来标注整个结构体实例对象的读写状态
零值为解锁状态,可以由不同的线程加锁和解锁
该锁可以被同时多个读取者持有或唯一个写入者持有
Lock() :写入锁,禁止其他协程进行读取和写入 当获取写入锁时,如果存在任何未解锁的写入锁或读取锁,将会阻塞直到所有锁均解锁 TryLock() bool Unlock() :解锁写入锁,未加写入锁会导致运行时错误 RLock() :读取锁,禁止其他线程写入但不禁止读取 读取锁可以被多个协程同时持有,所以多个协程同时获取读取锁是可行的 但是一定要注意获取锁和解除锁一定要成对出现,如果有锁未进行解锁操作,将导致该资源无法获取写入锁 同一个goroutine在持有读取锁的情况下再次尝试获取读取锁会导致死锁 根据实测,如果重复获取读取锁是在循环体或递归中会导致死锁 但是如果以字面量就是每行编写获取读取锁代码,连续多行获取仍然不会死锁,但是解锁需要同样数量的RUnlock()才行
TryRLock() bool
RUnlock():解除读取锁,如果未加写入锁会导致运行时错误
type WaitGroup
用于等待一组线程的结束
父线程调用Add方法来设定应等待的线程的数量
每个被等待的线程在结束时应调用Done方法
同时主线程调用Wait方法阻塞至所有线程结束
Add(delta int) :向内部计数加上delta(可以是负数) 如果内部计数器变为0,Wait方法阻塞等待的所有线程都会释放 如果计数器小于0,方法panic Done() :计数器的值减1,应在协程的最后执行 Wait() :阻塞直到计数器为0
type Pool
是一组单独保存和检索的临时对象的集合,类似于“池”
type Pool struct { New func() any }
Pool适合存储一些可能被重复使用,无状态或状态可重置的对象,多个goroutine同时使用一个Pool是安全的
Pool的作用是构建高效、线程安全的空闲列表,缓存已分配但未使用的项,以供以后重复使用,从而减轻垃圾回收器的压力
Pool不保证其中对象的可用性,如果Pool是一个对象的唯一引用,那么这个对象可能会被释放或回收,而没有任何通知
Pool中唯一的导出元素New是一个函数,该函数用于替换在获取一个存储对象时返回的nil,用于动态生成新的值
有一个非常重要的点,就是虽然Pool是并发安全的,但是New()函数的具体实现是我们自己编写的,如果在New()方法中存在对一些公共数据的修改,那么在编写的时候一定要注意并发安全
Pool会根据对这个“池”的引用数量或负载情况进行自动扩展和收缩
Pool的方法
Put(x any) :将值x放入Pool中 需要注意:由于x的值为any,并且内部没有进行类型的判断和断言 如果Put时不对x的数据类型做限制,那么Pool中就可能存在各种类型的数据,导致在Get后需要进行类型判断 Get() any :从池中随机获取一个项,并将其从Poll中移除 Get方法可能会在Pool中有成员时调用New方法返回一个新值