? 放在前面说的话
大家好,我是北 ??
本科在读,此为日常捣鼓.
如有不对,请多指教,也欢迎大家来跟我讨论鸭 ???
今天是我们「Go并发」系列的第三篇:「sync包并发同步原语(1)」;
Let’s get it!
sync包
再并发编程中同步原语也就是我们日常说的锁
保证多线程或多goroutine
在访问同一内存时,不出现混乱问题
Go语言提供的sync
包提供了常见的并发编程同步原语
sync.Mutex
sync.RWMutex
sync.WaitGroup
sync.Map
sync.Pool
sync.Once
sync.Cond
一、sync.Mutex
在Go并发任务中,容易出现多个goroutine
同时操作一个资源,这就会产生竞态问题。这时互斥锁就可以体现出它真正的作用了
1.sync.Mutex概念
Mutex 也称为互斥锁,互斥锁就是互相排斥的锁,它可以用作保护临界区的共享资源,保证同一时刻只有一个 goroutine 操作临界区中的共享资源。
控制共享资源访问方法
保证同一时间有且仅有一个goroutine
操作资源(临界区),其他goroutine
只能等待锁,直到前面的互斥锁释放,下一个goroutine
才能获取锁去操作资源,往后同理
多线程,随机唤醒一个
2. sync.Mutex有以下方法
Mutex 的 Lock
方法和 Unlock
方法要成对使用,不要忘记将锁定的互斥锁解锁,一般做法是使用 defer。
3.sync.Mutex 栗子
不加锁栗子
func main() {
var count = 0
var wg sync.WaitGroup
// 开启十个协程
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 一万叠加
for j := 0; j < 10000; j++ {
count++
}
}()
}
wg.Wait()
fmt.Println(count)
}
打印:73662正确打印:100000原因:多个goroutine在同一片资源出现竟态问题,叠加错误(有时候可能能够正常执行,打印正确,但并不代表后面不会出现错误)### 加锁优化栗子```gofunc main() { var count = 0 var wg sync.WaitGroup var m sync.Mutex // 开启十个协程 for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() // 一万叠加 for j := 0; j < 10000; j++ { m.Lock() count++ m.Unlock() } }() } wg.Wait() fmt.Println(count)}
打印:100000
4. sync.Mutex和sync.RWMutex比较
RWMutex是基于Mutex的,在Mutex的基础之上增加了读、写的信号量,并使用了类似引用计数的读锁数量,RWMutex 将对临界区的共享资源的读写操作做了区分,RWMutex 可以针对读写操作做不同级别的锁保护。
Mutex在大并发环境下,容易造成锁等待,对性能的影响较大。若某个读/写操作协程加了锁,其他协程就没必要处于等待状态了,也应该可以并发地访问共享变量,这时候,应使用RWMutex,让读/写操作并行,提高性能
二、sync.RWMutex
适用于并发读读,不能进行并发读写
1. sync.RWMutex概念
读锁与读锁兼容,读锁与写锁互斥,写锁与写锁互斥,只有在锁释放后才可以继续申请互斥的锁
可以同时申请多个读锁
有读锁时申请写锁将阻塞,有写锁时申请读锁将阻塞
只要有写锁,后续申请读锁和写锁都将阻塞
2.sync.RWMutex有以下方法
RWMutex 也称为读写互斥锁,读写互斥锁就是读取/写入互相排斥的锁。它可以由任意数量的读取操作的 goroutine 或单个写入操作的 goroutine 持有。 |方法|功能| | --- | --- | |Lock()|申请写锁| |Unlock()|申请释放写锁| |RLock()|申请读锁| |RUnlock()|申请释放读锁| |RLocker()|返回一个实现了Lock()和Unlock()方法的Locker接口|
3.sync.RWMutex栗子
func main() { var rm sync.RWMutex for i := 0; i < 3; i++ { go read(&rm, i) } time.Sleep(time.Second * 2)}func read(rm *sync.RWMutex, i int) { fmt.Println(i, "reader start") rm.RLock() fmt.Println(i, "reading") time.Sleep(time.Second * 1) rm.RUnlock() fmt.Println(i, "reader over")}
打印:
打印结果看得出来,2开始,还没有读完,1和0就相继跟上开始读了
三、sync.WaitGroup
1. time.sleep()和sync.WaitGroup比较
理论上wait
和sleep
是完全没有可比性的,一个用于线程间的通信,另一个用于线程阻塞一段时间。唯一相同的就是,sleep
方法和wait
方法都是用来使线程进入休眠状态的,对于中断信号,都可以进行响应和中断 不同:
语法使用:wait
方法必须配合synchronized一起使用,sleep
可以单独使用
唤醒方式:sleep
需要传递一个超时时间,因此,sleep
具有主动唤醒功能,而不需要传递任何参数的wait
只能被动唤醒
释放锁资源:wait
方法主动释放锁,sleep
不释放锁
线程进入状态:sleep
有时限等待状态,wait
无时限等待状态
2.sync.WaitGroup有以下方法
在并发操作中,生硬使用time.Sleep并不合适,反观前面的比较,sync.WaitGroup更适用于并发任务的同步实现
sync.WaitGroup内部维护着一个计数器,可增可减。当我们要启动Z个并发任务时,计时器总数要增加到Z,可以通过for循环Add()单个增加,也可以一下子增加到Z;每当一个任务结束时,Done()就要在计数器里-1,直到计数器为0时,调用Wait()表示等待并发任务执行完成。
3. sync.WaitGroup栗子
func main() { wg := sync.WaitGroup{} n := 10 wg.Add(n) // 计数器累加至n for i := 0; i <= n; i++ { go f(i, &wg) } wg.Wait() // 等待计数器值为0,告知main函数的主协程,其他协程执行完毕}func f(i int, wg *sync.WaitGroup) { fmt.Println(i) wg.Done() // 完成该协程后,计数器-1}
sync.WaitGroup一定要通过指针传值,不然进程会进入死锁状态
? 放在后面的话
本文我们介绍了 Go 语言中的基本同步原语sync.Mutex、sync.RWMutex、sync.WaitGroup的概念和简单应用,并分别对sync.Mutex和sync.RWMutex,time.sleep和sync.WaitGroup进行了比较。读写互斥锁可以对临界区的共享资源做更加细粒度的访问控制,不限制对临界区的共享资源的并发读,所以在读多写少的场景,我们可以使用读写互斥锁替代互斥锁,提升应用程序的性能。在并发中,我们并不知道完成这一应用程序,我们需要多长时间,time.sleep时有时限的且功能等在并发中,并不算优雅,故sync.WaitGroup更适用于任务编排,等待多个 goroutine 全部完成。
sync包中还有三个比较常用的锁,我们将会在下一篇细说。
原文:https://juejin.cn/post/7102779550815223816