go从0到1项目实战体系九:锁机制
2023-12-22 10:28:56
Golang中的锁机制主要包含互斥锁和读写锁.
1. 为什要加锁?
①. 现实场景:
a. 厕所是共享的,你要上厕所,就需要先开门再锁上锁.就是一个加锁的操作.
b. 红绿灯也是一种资源的共享,红灯就表示上锁,不能通行.
②. 同理,线程是共享的,所有要加锁.
a. 如果map和数组被多个goroute来修改,如果不加锁就有异常.
b. 多个协程同时读一个变量的值没有问题,是不用加锁的.
c. 一个变量同时读写也要加锁.
d. 发生共享的值?
1. 一个是全局变量.
2. 也可能map是通过参数传进来的.
③. 互斥锁 => 读和写频率一样的场景
读写锁 => 读多写少的场景
(1). 在并发情况下,多个线程或协程同时修改一个变量,不加锁的结果:
func main() {
var a = 0
for i := 0; i < 1000; i++ {
go func(idx int) {
a += 1
fmt.Println(a)
}(i)
}
time.Sleep(time.Second) // 等待1s结束主程序,确保所有协程执行完
}
注:
①. 从理论上,函数里每次递增a的值的,应该有1000个不同的值输出.
②. 执行结果:
$ go run test.go |sort|uniq |wc -l
998 // 绝对出现了重复值
$ go run test.go |sort|uniq |wc -l
1000
a. 打印的值不是按顺序来的,因为是协程在执行.
③. 协程依次执行顺序:
从寄存器读取a的值 -> 再做加法运算 -> 最后写到寄存器.
④. 并发场景:
a. 一个协程取出a的值是3,正在做加法运算(还未写回寄存器).
b. 同时另一个协程去取,取出了同样的a的值3.
c. 最终,两个协程产出的结果相同,a相当于只增加了1.
⑤. 互斥锁是传统并发程序对共享资源进行控制访问的主要手段.
a. 在Go中主要使用sync.Mutex的结构体表示.
b. 互斥锁应该是成对出现,同级别互斥锁不能嵌套使用,不可以再对锁加锁.
c. 也不可以多次对一个锁解锁.
⑥. 只有一个能连进去,它释放后,下一个才能进去,性能比较低.
a. 因为只要无论是读或写,都上会锁,只有一个才能进去.
(2). 加锁:
①. 有多个线程要同时操作一个资源.如果不加锁,是不是别人也可以进去.
②. 在并发的情况下,多个线程或协程同时去修改一个变量:
a. 使用锁能保证在某一时间点内,只有一个协程或线程修改这一变量.
b. 如:我正在处理a(锁定),谁都别和我抢,等我处理完了(解锁),你们再处理.
=> 这样就实现了,同时处理a的协程只有一个,就实现了同步.
③. 如果要避免红绿灯,就要架设高架桥.后果是共享的资源变成了独占的资源了,就不用加锁了.
2. 加锁:
sync包里提供了Locker接口、互斥锁Mutex、读写锁RWMutex用于处理并发过程中可能出现同时两个或多个协程(或线程)读或写同一个变量的情况.
2.1 互斥锁Mutex:
import("sync")
func main() {
var a = 0
var lock sync.Mutex
for i := 0; i < 1000; i++ {
go func(idx int) {
lock.Lock()
// defer lock.Unlock() // 加了这句话,下面的Unlock就不用加
a += 1
lock.Unlock()
fmt.Printf("goroutine %d, a=%d\n", idx, a)
}(i)
}
time.Sleep(time.Second)
}
注:
①. 执行的结果总是1000个不重复的值,但是打印的值不是按顺序来的.
②. 一个互斥锁只能同时被一个goroutine锁定,其它goroutine将阻塞直到互斥锁被解锁(重新争抢对互斥锁的锁定).
(1). 证明:
func main() {
ch := make(chan struct{}, 2)
var l sync.Mutex
go func() {
l.Lock()
defer l.Unlock()
fmt.Println("goroutine1: 我会锁定大概2s")
time.Sleep(time.Second * 2)
fmt.Println("goroutine1: 我解锁了,你们去抢吧")
ch <- struct{}{}
}()
go func() {
fmt.Println("groutine2: 等待解锁")
l.Lock()
defer l.Unlock()
fmt.Println("goroutine2: 我也解锁了")
ch <- struct{}{}
}()
// 等待 goroutine 执行结束
for i := 0; i < 2; i++ {
<-ch
}
}
注:
groutine2: 等待解锁
goroutine1: 我会锁定大概2s
goroutine1: 我解锁了,你们去抢吧
goroutine2: 我也解锁了
2.2 读写锁:
(1). 4个方法:
①. 写操作的锁定和解锁:
func (*RWMutex) Lock
func (*RWMutex) Unlock
②. 读操作的锁定和解锁:
func (*RWMutex) Rlock
func (*RWMutex) RUnlock
(2) 理解:
①. 规则:
a. 当有一个goroutine获得写锁定,其它无论是读锁还是写锁都将阻塞直到写解锁.
b. 当有一个goroutine获得读锁定,其它读锁定仍然可以继续.
c. 当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定.
1. 所以这里的读锁定(RLock),目的是告诉写锁定,有很多人正在读取数据,你不能操作.
2. 等它们读解锁完了,你再来写锁定.
②. 读写锁可以分别针对读操作和写操作进行分别锁定,有一定的性能提升:
a. 对于多个写操作,以及写操作和读操作之间是互斥的.
b. 对于同时多个读操作之前却非互斥关系,这是读写锁性能高于互斥锁的主要原因.
③. 总结:
a. 同时只能有一个goroutine能够获得写锁定.
b. 同时可以有任意多个gorouinte获得读锁定.
c. 同时只能存在写锁定或读锁定(读和写互斥).
④. 举例:
a. 教室里面的黑板,全班的同学都可以读黑板,但只能老师才能够改变黑板上面的文字.
b. 如果有两个老师同时更改黑板的内容,会造成同学们无法确定该读谁写的文字.
c. 其中,一个老师在写文字的时候,需要把这一过程上锁,一旦上了读写锁.
d. 第二个老师只能等待第一个老师写完才可以进行书写.
(3). 读写锁共存:
var count int
var rw sync.RWMutex
func main() {
ch := make(chan struct{}, 10)
for i := 0; i < 5; i++ {
go read(i, ch)
}
for i := 0; i < 5; i++ {
go write(i, ch)
}
for i := 0; i < 10; i++ {
<-ch
}
}
func read(n int, ch chan struct{}) {
rw.RLock()
fmt.Printf("goroutine %d 进入读操作...\n", n)
v := count
fmt.Printf("goroutine %d 读取结束,值为:%d\n", n, v)
rw.RUnlock()
ch <- struct{}{}
}
func write(n int, ch chan struct{}) {
rw.Lock()
fmt.Printf("goroutine %d 进入写操作...\n", n)
v := rand.Intn(1000)
count = v
fmt.Printf("goroutine %d 写入结束,新值为:%d\n", n, v)
rw.Unlock()
ch <- struct{}{}
}
结果:
goroutine 1 进入读操作...
goroutine 1 读取结束,值为:0
goroutine 4 进入读操作...
goroutine 4 读取结束,值为:0
goroutine 2 进入读操作...
goroutine 2 读取结束,值为:0 // 在上面过程中,读都是0
goroutine 4 进入写操作...
goroutine 4 写入结束,新值为:81 // 在写操作之后,值是81
goroutine 0 进入读操作...
goroutine 0 读取结束,值为:81
goroutine 3 进入读操作...
goroutine 3 读取结束,值为:81
goroutine 0 进入写操作...
goroutine 0 写入结束,新值为:887
goroutine 1 进入写操作...
goroutine 1 写入结束,新值为:847
goroutine 2 进入写操作...
goroutine 2 写入结束,新值为:59
goroutine 3 进入写操作...
goroutine 3 写入结束,新值为:81
(4). 只有读锁:
var m *sync.RWMutex
func main() {
m = new(sync.RWMutex)
// 多个同时读
go read(1)
go read(2)
time.Sleep(2*time.Second)
}
func read(i int) {
println(i,"read start")
m.RLock()
println(i,"reading")
time.Sleep(1*time.Second)
m.RUnlock()
println(i,"read over")
}
结果:
1 read start
1 reading
2 read start
2 reading
1 read over
2 read over
多个读操作同时读一个操作,虽然加了锁,但都是读是不受影响的.
(5). 读写锁:
var m *sync.RWMutex
func main() {
m = new(sync.RWMutex)
// 写的时候啥也不能干
go write(1)
go read(2)
go write(3)
time.Sleep(2*time.Second)
}
func read(i int) {
println(i,"read start")
m.RLock()
println(i,"reading")
time.Sleep(1*time.Second)
m.RUnlock()
println(i,"read over")
}
func write(i int) {
println(i,"write start")
m.Lock()
println(i,"writing")
time.Sleep(1*time.Second)
m.Unlock()
println(i,"write over")
}
结果:
2 read start
2 reading
3 write start
1 write start
2 read over
3 writing
文章来源:https://blog.csdn.net/m0_68635815/article/details/135146284
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!