1. 并发安全和锁
两个概念:
①临界资源:一次只允许一个进程独占访问(使用的)资源
②临界区:进程中访问临界资源的程序段
在并发时可能会存在多个goroutine同时操作一个临界资源,这种情况会发生竞态问题(数据竞争),数据竞争会导致最后的结果与期待的不符。
这个时候我们可以通过给临界区加锁的方式来控制对共享资源的访问。
1.1互斥锁
互斥锁可以保证任何时间只允许一个goroutine可以访问共享资源
go中使用sync.Mutex来实现互斥锁
var lock sync.Mutex
lock.Lock() // 加锁
//临界资源
lock.Unlock() // 解锁
多个goroutine同时等待一个锁时,唤醒的策略是随机的
Unlock未加锁的Mutex会Panic
1.2读写互斥锁
在读多写少的情况下,使用读写互斥锁是更好的选择
读写锁分为读锁和写锁
读写锁在Go语言中使用sync.RWMutex
var rwlock sync.RWMutex
rwlock.Lock() // 加写锁
//临界资源
rwlock.Unlock() // 解写锁
rwlock.RLock() // 加读锁
rwlock.RUnlock() // 解读锁
当一个goroutine获取读锁后,其他goroutine也能继续获得读锁,但是不能获取写锁
当一个goroutine获取写锁后,其他goroutine既不能获取读锁,也不能获取写锁
2.Sync
2.1 sync.WaitGroup
在并发时使用time.Sleep是不合适的
通常使用sync.WaitGroup来实现并发同步。
方法 | 功能 |
---|---|
Add (delta) | 计数器+delta |
Done() | 计数器-1 |
Wait() | 阻塞,直到计数器变为0 |
注意:
如果计数器<0,会panic
Done()等价于Add(-1)
当计数器为0的时候,阻塞在Wait方法的goroutine都会被释放
Add()一定要在Wait()前设置好
2.2 sync.Once
sync.Once 只有一个Do方法,它可以确保某些操作在高并发的场景下只执行一次
例如只加载一次配置文件、只关闭一次通道
对于加载配置文件:
预先初始化一个变量(比如在init函数中完成初始化)会增加程序的启动耗时,而且这个初始化不一定用得上,这个时候就可以用sync.Once
ps.请尽量避免init函数的使用
2.3 sync.Pool
并发池,负责安全地保存一组对象
方法 | 功能 |
---|---|
Get() interface{} | 从并发池中取出元素 |
Put(interface{}) | 将一个对象加入并发池 |
Pool的目的是缓存申请但未使用的item用于之后的重用,以减轻GC的压力
Pool可以安全的被多个线程同时使用
Pool中保存的任何item都可能随时不做通告的释放掉
2.4 sync.Map
GO中内置的map不是并发安全的
在高并发的情况下会:
fatal error: concurrent map writes
sync包中的map是并发安全的,且“开箱即用”,不需要make()初始化就可以直接使用
方法 | 功能 |
---|---|
Store(interface {},interface {}) | 添加元素 |
Load(interface {}) interface {} | 检索元素 |
Delete(interface {}) | 删除元素 |
LoadOrStore(interface {},interface {}) (interface {},bool) | 检索或添加之前不 存在的元素 |
Range | 遍历元素 |
sync.Map的使用场景:
读取map的操作需求远大于写入map的操作
多个goroutine对map的操作不相交时
原子操作(atomic包)
上文中所提到的加锁操作会比较耗时、代价很高
对基本数据类型可以使用原子操作来保证并发安全,原子操作在用户态就可以完成,性能比加锁更好
channel
channel的功能:
①goroutine之间的通讯
②实现goroutine同步(锁)
无缓冲的channel,在缺省情况下发送和接收会一直阻塞,这种特性可以实现goroutine之间的同步
③实现定时器和计时器
定时器:
在time包中,Timer类型中包含一个只读的channel
type Timer struct {
C <-chan Time
// 内含隐藏或非导出字段</