go语言 sync 包简介及使用
文章目录
sync 包的简介
Go语言的sync包提供了常见的并发编程控制锁;
在并发编程中锁的主要作用是保证多个线程或者 goroutine在访问同一片内存时不会出现混乱;
golang 中使用 go 语句来开启一个新的协程。 goroutine 是非常轻量的,除了给它分配栈空间,它所占用的内存空间是微乎其微的;
但当多个 goroutine 同时进行处理的时候,就会遇到比如同时抢占一个资源,某个 goroutine 等待另一个 goroutine 处理完某一个步骤之后才能继续的需求。
在 golang 的官方文档上,作者明确指出,golang 并不希望依靠共享内存的方式进行进程的协同操作。
而是希望通过管道 channel 的方式进行;
但是一些特殊情况下,我们依然需要用到锁,所以 sync 包提供了我们需要的功能.
整个包都围绕这 Locker 进行,这是一个 interface:
type Locker interface {
Lock()
Unlock()
}
接口中定义了2个方法,Lock() 和 Unlock()
为什么需要锁
在并发的情况下,多个线程或协程同时去修改一个变量 , 出现资源抢占 ,数据不一致的问题:
package main
import (
"fmt"
"time"
)
func main() {
var a = 0
// 启动足够多个协程
for i := 0; i < 1000000; i++ {
go func(idx int) {
a += 1
fmt.Printf("goroutine %d, a=%d\n", idx, a)
}(i)
}
// 主程序等待1s 确保所有协程执行完
time.Sleep(time.Second)
}
上面的程序中一次启动了足够多的协程,以达到抢占资源目的,你在测试时可以把协程数量变小来看看会不会出现资源抢占.
我们来看下结果:
结果中俩个不同的协程输出相同的结果
goroutine 19997, a=20088
goroutine 20109, a=20088
这明显不是我们想要的结果, 协程依次从寄存器读取 a 的值 -> 然后做加法运算 -> 最后写会寄存器;
此时一个协程 goroutine 19997 取出 a 的值 20088,正在做加法运算(还未写回寄存器);
同时另一个协程 goroutine 20109 也要去取a的值,取出了同样的 a 的值 20088 (因为上一个协程运行结果还没有写回寄存器);
最终导致的结果是两个协程产出的结果相同.
sync.Mutex
Mutex 互斥锁实现了2个方法
func (m *Mutex) Lock()
func (m *Mutex) Unlock()
对一个未锁定的互斥锁解锁将会产生运行时错误。
一个互斥锁只能同时被一个 goroutine 锁定,其它 goroutine 将阻塞直到互斥锁被解锁(重新争抢对互斥锁的锁定)
我们重新修改下上面的测试代码:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var a = 0
// 启动足够多个协程
var lock sync.Mutex
for i := 0; i < 1000000; i++ {
go func(idx int) {
lock.Lock()
defer lock.Unlock()
a += 1
fmt.Printf("goroutine %d, a=%d\n", idx, a)
}(i)
}
// 主程序等待1s 确保所有协程执行完
time.Sleep(time.Second)
}
上面代码我们引入了 sync 包,
代码里定义了 Mutex 类型变量 lock,
协程里面我们给互斥锁加锁,
确保协程执行完成后, defer lock.Unlock() 执行解锁操作.
查看结果:
结果不仅没有发现抢占资源导致的输出重复,而且输出结果顺序递增.
Mutex类型互斥锁原理
平时所说的锁定,其实就是去锁定互斥锁,而不是说去锁定一段代码;
这和其他编程语言有所区别,例如java 中我们会用同步锁锁定一段代码,确保多线程并发时只有一个线程可以控制运行此代码段,知道释放同步锁;
而go语言是在 goroutine 中锁定互斥锁 ,其他 goroutine 执行到有锁的地方时,它获取不到互斥锁的锁定,会阻塞在那里等待,从而达到控制同步的目的.
sync.RWMutex
RWMutex 读写锁 基于 Mutex 实现
读写锁的特点 : 它是针对读写操作的互斥锁,读写锁与互斥锁最大的不同就是可以分别对 读、写 进行锁定。
一般用在大量读操作、少量写操作的情况.
- 同时只能有一个 goroutine 能够获得写锁定
- 同时可以有任意多个 gorouinte 获得读锁定
- 同时只能存在写锁定或读锁定(读和写互斥)
当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;
当有一个 goroutine 获得读锁定,其它读锁定任然可以继续;
当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定;
所以说这里的读锁定(RLock)目的其实是告诉写锁定:有很多人正在读取数据,你需要排队等待;
实现的方法:
func (rw *RWMutex) Lock()
func (rw *RWMutex) Unlock()
func (rw *RWMutex) RLock()
func (rw *RWMutex) RUnlock()
- 读锁定(RLock),对读操作进行锁定
- 读解锁(RUnlock),对读锁定进行解锁
- 写锁定(Lock),对写操作进行锁定
- 写解锁(Unlock),对写锁定进行解锁
上面的锁成对使用,不能互相拆分混用,会发生运行时错误
package main
import (
"fmt"
"sync"
"time"
)
var m *sync.RWMutex
var val = 0
func main() {
m = new(sync.RWMutex)
for i := 0; i < 5; i++ {
go read(1)
}
for j := 0; j < 5; j++ {
go write(2)
}
for m := 0; m < 5; m++ {
go read(3)
}
time.Sleep(18 * time.Second)
}
func read(i int) {
m.RLock()
println("读: ", i, val)
time.Sleep(3 * time.Second)
println("读结束")
m.RUnlock()
}
func write(i int) {
m.Lock()
val = 10
println("写: ", i, val)
time.Sleep(3 * time.Second)
println("写结束")
m.Unlock()
}
上面 read 方法应用了读锁RLock;
write 方法应用了写锁 Lock;
为了验证协程抢夺资源, 读写方法中我们让程序休眠一定的时间;
main 函数中的 time.Sleep 是为防止主程序提前退出,goroutine 协程函数还未执行完.
查看运行结果:
# zhangzhi @ ZhangZhi-MacBook-Pro in ~/code/go/program [19:08:18]
$ go run rwlock.go
读: 1 0
读结束
写: 2 10
写结束
读: 3 10
读: 3 10
读: 1 10
读: 1 10
读: 3 10
读: 1 10
读: 1 10
读: 3 10
读: 3 10
读结束
读结束
读结束
读结束
读结束
读结束
读结束
读结束
读结束
写: 2 10
写结束
写: 2 10
写结束
写: 2 10
注意:每次执行的结果都有差异,当你复制上面代码运行,结果和上面有所不同
但是:结果展示出来的规律却是一致的:
规律一:[同时可以有任意多个 gorouinte 获得读锁定]
RWMutex 读锁可以并发多个执行,从上面read 程序和程序执行输出的内容来看
说明在加上读取锁时,其他 goroutine 依然可以并发多个 读 访问.
规律二: [同时只能有一个 goroutine 能够获得写锁定]
RWMutex 写获得锁定时,不论程序休眠多长时间,一定会输出 写结束,其他 goroutine 才能获得锁资源.
规律三: [同时只能存在写锁定或读锁定(读和写互斥)]
读虽然可以同时多个 goroutine 来锁定,但是写锁定之前其他多个读锁定必须全部释放锁.
写锁定获得锁时,其他 读 或者 写 都无法再获得锁,直到此 goroutine 写结束,释放锁后,其他 goroutine 才会争夺.
所以 读和写 的俩种锁是互斥的.
RWMutex 读写锁使用于读多写少的业务逻辑
sync.WaitGroup
它的使用场景是在一个goroutine等待一组goroutine执行完成.
WaitGroup拥有一个内部计数器;
当计数器等于0时,则Wait()方法会立即返回;
否则它将阻塞执行Wait()方法的goroutine直到计数器等于0时为止;
增加计数器,使用Add(int)方法。
减少计数器,我们可以使用Done()(将计数器减1),
也可以传递负数给Add方法把计数器减少指定大小,Done()方法底层就是通过Add(-1)实现的.
/*
* @Author: your name
* @Date: 2021-01-13 19:45:43
* @LastEditTime: 2021-01-13 19:45:44
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: /program/waitgroup.go
*/
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
// 计数加 1
wg.Add(1)
go func(i int) {
// 计数减 1
defer wg.Done()
time.Sleep(time.Second * time.Duration(i))
fmt.Printf("goroutine%d 结束\n", i)
}(i)
}
// 等待执行结束
wg.Wait()
fmt.Println("所有 goroutine 执行结束")
}
执行结果:
# zhangzhi @ ZhangZhi-MacBook-Pro in ~/code/go/program [19:46:01]
$ go run waitgroup.go
goroutine0 结束
goroutine1 结束
goroutine2 结束
goroutine3 结束
goroutine4 结束
所有 goroutine 执行结束