2.16 Go之sync包与锁:限制线程对变量的访问
sync包
sync
包提供了的锁:
-
Mutex
:互斥锁 -
RWMutex
:读写锁
为什么需要锁
sync
包中的两个核心方法:
-
Lock
:加锁 -
Unlock
:解锁
在并发的情况下,多个线程或协程同时其修改一个变量,使用锁能保证在某一时间内,只有一个协程或线程修改这一变量。
不使用锁时,在并发的情况下可能无法得到想要的结果
非并发场景示例:
package main
import (
"fmt"
"time"
)
/*
设计一个并发场景,不使用锁看看结果如何
*/
func main() {
// 设置一个变量a,使用两个协程对其值进行累加
var a = 0
for i := 0; i < 1000; i++ {
// 开启一个协程
go func(index int) {
a += 1
fmt.Println(a)
}(i)
}
// 每次累加以后协程暂歇两秒
time.Sleep(time.Second)
}
结果集:
968
966
969
970
971
27
分析:
可以明显看到a
的值并不是按顺序递增输出的
原因分析:
协程的执行过程:
-
从寄存器读取 a 的值;
-
然后做加法运算;
-
最后写到寄存器。
按照上面的顺序,假如有一个协程取得a
的值为3
,然后执行加法运算,此时又有一个协程对a
进行取值,得到的值同样是3
,最终两个协程的返回结果是相同的。
而锁的概念就是,当一个协程正在处理a
时将a
锁定,其它协程需要等待该协程处理完成并将a
解锁后才能再进行操作,也就是说同时处理a
的协程只能有一个,从而避免上面示例中的情况出现。
互斥锁
上诉情况加一个互斥锁就可以解决。
互斥锁的特点:
一个互斥锁只能同时被一个goroutine
锁定,其它goroutine
将阻塞直到互斥锁被解锁(重新争抢对互斥锁的锁定)
package main
import (
"fmt"
"sync"
"time"
)
/*
设置一个累加变量
使用互斥锁对变量进行锁定控制
*/
func main() {
// 累加变量
var a = 0
// 锁变量
var lock sync.Mutex
// 循环累加变量
for i := 0; i < 1000; i++ {
// 开启一个协程
go func(index int) {
// 进行加锁操作
lock.Lock()
// 最后的处理是释放掉锁
defer lock.Unlock()
// 进行累加
a += 1
fmt.Printf("goruntine %d, a = %d\n", index, a)
}(i)
}
// 等待一秒结束主程序
// 确保所有协程执行完成
time.Sleep(time.Second)
}
结果集:
goruntine 962, a = 964
goruntine 963, a = 965
goruntine 4, a = 966
goruntine 966, a = 967
goruntine 965, a = 968
分析:
可以看到虽然协程存在争抢行为,但是该行为并没有影响到累加变量a
的累加过程
互斥锁场景示例:
package main
import (
"fmt"
"sync"
"time"
)
/*
开启两个协程
创建一个结构体对象
使用两个协程对该变量进行修改
*/
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)
fmt.Println("goroutine1: 一已解锁!")
ch <- struct{}{}
}()
// 协程二为等待争抢协程
go func() {
fmt.Println("goroutine: 等待解锁")
// 上锁
l.Lock()
// 最后解锁
defer l.Unlock()
fmt.Println("goroutine: 二已解锁!")
// 最后关闭将变量放回协程
ch <- struct{}{}
}()
// 等待goroutine执行结束
for i := 0; i < 2; i++ {
<-ch
}
}
结果示例:
goroutine: 等待解锁
goroutine1: 我会锁定2s
goroutine1: 一已解锁!
goroutine: 二已解锁!
分析:
注意看两个协程的上锁位置。
协程一是一开始就上锁所以此时是协程二先执行
然后协程二开始上锁此时程序执行了协程一当中的代码
最后执行完毕施放了以后再执行协程二当中的代码
读写锁
读写锁有四种方法:
写的上锁和解锁:
func(*RWMutex) Lock
func(*RWMutex) Unlock
读的上锁和解锁:
func (*RWMutex) Rlock
func (*RWMutex) RUnlock
读写锁的区别:
-
一个
goroutine
获得写锁,其他的任何读锁或者写锁都要阻塞到改写解锁 -
一个
goroutine
获得读锁,其他读锁可以继续锁定,但是写锁不可以 -
一个或者多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定
这里的读锁定(RLock
)目的其实是告诉写锁定,有很多协程或者进程正在读取数据,写操作需要等它们读(读解锁)完才能进行写(写锁定)
概括:
-
同时只能有一个
goroutine
能够获得写锁定; -
同时可以有任意多个
gorouinte
获得读锁定; -
同时只能存在写锁定或读锁定(读和写互斥)
示例代码:
package main
import (
"fmt"
"math/rand"
"sync"
)
/*
开启两个协程
一个读
一个写
同时对结构体当中的属性进行操作
体会读写锁的特性
*/
// 声明两个变量,一个int的总数变量,一个读写锁变量
var count int
var rw sync.RWMutex
func main() {
// 设置结构体变量
ch := make(chan struct{}, 10)
// 循环进行读或者写的操作
for i := 0; i < 5; i++ {
// 调用read方法
go read(i, ch)
}
// 循环进行写的操作
for i := 0; i < 5; i++ {
// 调用write方法
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)
fmt.Printf("goroutine %d 写入结束, 新的值为:%d\n", n, v)
// 释放写锁
rw.Unlock()
// 放回
ch <- struct{}{}
}
特征:
上诉的函数中说明读写互斥
展示多个协程读取同一个变量:
package main
import (
"sync"
"time"
)
/*
定义一个读写锁变量
定义一个读取函数
构建多个协程调用该函数
*/
var m *sync.RWMutex
func moreRead(i int) {
println(i, "开始读取!")
// 上锁
m.RLock()
// 读取
println(i, "reading")
// 休眠
time.Sleep(1*time.Second)
// 施放读锁
m.RUnlock()
println(i, "读取结束!")
}
// 调用多个读取的函数
func main() {
m = new(sync.RWMutex)
// 多个开始读取--->读锁不互斥
go moreRead(1)
go moreRead(2)
// 休眠
time.Sleep(2*time.Second)
}
结果集:
2 开始读取!
2 reading
1 开始读取!
1 reading
1 读取结束!
2 读取结束!
可以观察到读锁之间不互斥
读写互斥,所以写操作开始的时候,读操作必须要等写操作进行完才能继续,不然读操作只能继续等待
package main
import (
"sync"
"time"
)
/*
声明一个读写锁变量
定义一个读函数
定义一个写函数
每个函数都是先开始进行读取(写入)
上锁
读取(写入)内容
释放锁
打印结果
*/
var variableM *sync.RWMutex
// 实际调用
func main() {
variableM = new(sync.RWMutex)
// 写入的时候与读取或者写入互斥
go writing(1)
go reading(2)
go writing(3)
// 等待时间
time.Sleep(2*time.Second)
}
// 读函数
func reading(i int) {
println(i, "开始读取!")
// 上锁
variableM.RLock()
// 读取内容
println(i, "读取中...")
// 等待
time.Sleep(1*time.Second)
// 释放锁
variableM.RUnlock()
// 打印结果
println(i, "读取结束!")
}
// 写函数
func writing(i int) {
println(i, "开始写入!")
// 上锁
variableM.Lock()
// 写入内容
println(i, "写入中...")
// 等待
time.Sleep(1*time.Second)
// 释放锁
variableM.Unlock()
// 打印结果
println(i, "写入结束")
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!