2.16 Go之sync包与锁:限制线程对变量的访问

2.16 Go之sync包与锁:限制线程对变量的访问

sync包

sync包提供了的锁:

  • Mutex:互斥锁

  • RWMutex:读写锁

为什么需要锁

sync包中的两个核心方法:

  1. Lock:加锁

  2. 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, "写入结束")
}

 

posted @   俊king  阅读(91)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示