Go 语言入门很简单:读写锁(下)

简介: 这一篇文章我们来介绍 Go 语言帮我们实现的标准库的 sync.RWMutex{} 读写锁。

读和写同时进行

虽然我们同一时间存款问题通过互斥锁得到了解决。但是如果同时存款与查询余额呢?

package main
import (
  "fmt"
  "sync"
)
type Account struct {
  name    string
  balance float64
  mux     sync.Mutex
}
//
func (a *Account) Deposit(amount float64) {
  a.mux.Lock() // lock
  a.balance += amount
  a.mux.Unlock() // unlock
}
func (a *Account) Balance() float64 {
  return a.balance
}
func main() {
  var wg sync.WaitGroup
  user := &Account{}
  user.name = "xiaoW"
  n := 1000
  wg.Add(n)
  for i := 1; i <= n; i++ {
    go func() {
      user.Deposit(1000)
      wg.Done()
    }()
  }
  // 查询余额
  wg.Add(n)
  for i := 1; i <= n; i++ {
    go func() {
      _ = user.Balance()
      wg.Done()
    }()
  }
  wg.Wait()
  fmt.Printf("%s's account banlance has %.2f $.", user.name, user.Balance())
}

然后我们运行代码,就又出现了线程竞争的问题:

$  go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c0000ba010 by goroutine 73:
  main.(*Account).Balance()
      /home/wade/GoProjects/Go RWMutex/v2/main.go:22 +0x44
  main.main.func2()
      /home/wade/GoProjects/Go RWMutex/v2/main.go:43 +0x32
Previous write at 0x00c0000ba010 by goroutine 72:
  main.(*Account).Deposit()
      /home/wade/GoProjects/Go RWMutex/v2/main.go:17 +0x84
  main.main.func1()
      /home/wade/GoProjects/Go RWMutex/v2/main.go:35 +0x46
Goroutine 73 (running) created at:
  main.main()
      /home/wade/GoProjects/Go RWMutex/v2/main.go:42 +0x1ba
Goroutine 72 (finished) created at:
  main.main()
      /home/wade/GoProjects/Go RWMutex/v2/main.go:34 +0x15e
==================
panic: sync: negative WaitGroup counter
goroutine 2018 [running]:
sync.(*WaitGroup).Add(0xc0000b4010, 0xffffffffffffffff)
        /usr/local/go/src/sync/waitgroup.go:74 +0x2e5
sync.(*WaitGroup).Done(...)
        /usr/local/go/src/sync/waitgroup.go:99
main.main.func2(0xc0000ba000, 0xc0000b4010)
        /home/wade/GoProjects/Go RWMutex/v2/main.go:44 +0x5d
created by main.main
        /home/wade/GoProjects/Go RWMutex/v2/main.go:42 +0x1bb
exit status 2

同理,我们需要对查询余额作同样的加锁处理:

func (a *Account) Balance() (balance float64) {
  a.mux.Lock()
  balance = a.balance
  a.mux.Unlock()
  return balance
}

如果发生读写阻塞呢?我们利用 time.Sleep() 来模拟线程阻塞的过程:

package main
import (
  "log"
  "sync"
  "time"
)
type Account struct {
  balance float64
  mux     sync.Mutex
}
//
func (a *Account) Deposit(amount float64) {
  a.mux.Lock() // lock
  time.Sleep(time.Second * 2)
  a.balance += amount
  a.mux.Unlock() // unlock
}
func (a *Account) Balance() (balance float64) {
  a.mux.Lock()
  time.Sleep(time.Second * 2)
  balance = a.balance
  a.mux.Unlock()
  return balance
}
func main() {
  wg := &sync.WaitGroup{}
  user := &Account{}
  n := 5
  wg.Add(n)
  for i := 1; i <= n; i++ {
    go func() {
      user.Deposit(1000)
      log.Printf("写:存款: %v", 1000)
      wg.Done()
    }()
  }
  wg.Add(n)
  for i := 1; i <= n; i++ {
    go func() {
      log.Printf("读:余额: %v", user.Balance())
      wg.Done()
    }()
  }
  wg.Wait()
}

我们在程序中,每隔两秒处理一次存款和查询操作,总共发生 5 次存款和 5 次查询,那么就需要 20 秒来执行这个程序。如果存款可以接受 2 秒的时间,但是读取应该只需要更快才对,即查询操作不应该发生阻塞。

$ go run -race main.go                                                                                                 
2022/02/28 14:31:43 写:存款: 1000
2022/02/28 14:31:45 写:存款: 1000
2022/02/28 14:31:47 写:存款: 1000
2022/02/28 14:31:49 写:存款: 1000
2022/02/28 14:31:51 写:存款: 1000
2022/02/28 14:31:53 读:余额: 5000
2022/02/28 14:31:55 读:余额: 5000
2022/02/28 14:31:57 读:余额: 5000
2022/02/28 14:31:59 读:余额: 5000
2022/02/28 14:32:01 读:余额: 5000

读写锁:sync.RWMutex

Mutex 将所有的 goroutine 视为平等的,并且只允许一个 goroutine 获取锁。针对这种情况,读写锁就该被派上用场了。


RWMutex 是 Go 语言中内置的一个 reader/writer 锁,用来解决读者-写者问题(Readers–writers problem)。任意数量的读取器可以同时获取锁,或者单个写入器可以获取锁。 这个想法是读者只关心与写者的冲突,并且可以毫无困难地与其他读者并发执行。


Go 的读写锁的特点:多读单写。 RWMutex 结构更灵活,支持两类 goroutine:readers 和 writers。 在任意一时刻,一个 RWMutex 只能由任意数量的 readers 持有,或者只能由一个 writers 持有。


读写锁的四个方法

  • RLock():此方法尝试获取读锁,并会阻塞直到被获取
  • RUnlock():解锁读锁
  • Lock():获取写锁,阻塞直到被获取
  • UnLock():释放写锁
  • RLocker():该方法返回一个指向 Locker 的指针,用于获取和释放读锁


读写锁演示

把互斥锁改为读写锁也很简单,只需要把 sync.Mutex 换成 sync.RWMutex ,然后在读操作的地方改为 RLock(),释放读锁改为 RUnlock()

package main
import (
  "log"
  "sync"
  "time"
)
type Account struct {
  balance float64
  mux     sync.RWMutex // 读写锁
}
//
func (a *Account) Deposit(amount float64) {
  a.mux.Lock() // 写锁
  time.Sleep(time.Second * 2)
  a.balance += amount
  a.mux.Unlock() // 释放写锁
}
func (a *Account) Balance() (balance float64) {
  a.mux.RLock() // 读锁
  time.Sleep(time.Second * 2)
  balance = a.balance
  a.mux.RUnlock() // 释放读锁
  return balance
}
func main() {
  wg := &sync.WaitGroup{}
  user := &Account{}
  n := 5
  wg.Add(n)
  for i := 1; i <= n; i++ {
    go func() {
      user.Deposit(1000)
      log.Printf("写:存款: %v", 1000)
      wg.Done()
    }()
  }
  wg.Add(n)
  for i := 1; i <= n; i++ {
    go func() {
      log.Printf("读:余额: %v", user.Balance())
      wg.Done()
    }()
  }
  wg.Wait()
}


明显能感觉到读操作变快了,发生一次写之后,直接发生 6 次读操作,说明读操作是同时进行的,存款 1000 一次后,6 次读操作都是 1000 元,说明结果是正确的。

2022/02/28 14:42:50 写:存款: 1000
2022/02/28 14:42:52 读:余额: 1000
2022/02/28 14:42:52 读:余额: 1000
2022/02/28 14:42:52 读:余额: 1000
2022/02/28 14:42:52 读:余额: 1000
2022/02/28 14:42:52 读:余额: 1000
2022/02/28 14:42:52 读:余额: 1000
2022/02/28 14:42:54 写:存款: 1000
2022/02/28 14:42:56 写:存款: 1000
2022/02/28 14:42:58 写:存款: 1000


总结

本文从读者-写者问题出发,回顾了互斥锁的案例:一个银行账户存款和查询的竞争问题的出现以及解决方法。最后引出 Go 自带的读写锁 sync.RWMutex

读写锁的特点是多读单写,一个 RWMutex 只能由任意数量的 readers 持有,或者只能由一个 writers 持有。我们可以利用读写锁来锁定某个操作以防止其他例程/线程在处理它时更改值,防止程序出现不可预测的错误。最后,可以利用读写锁弥补互斥锁的缺陷,用来加快程序的读操作,减少程序的运行时间。

目录
打赏
0
0
0
0
6
分享
相关文章
【二叉树遍历入门:从中序遍历到层序与右视图】【LeetCode 热题100】94:二叉树的中序遍历、102:二叉树的层序遍历、199:二叉树的右视图(详细解析)(Go语言版)
本文详细解析了二叉树的三种经典遍历方式:中序遍历(94题)、层序遍历(102题)和右视图(199题)。通过递归与迭代实现中序遍历,深入理解深度优先搜索(DFS);借助队列完成层序遍历和右视图,掌握广度优先搜索(BFS)。文章对比DFS与BFS的思维方式,总结不同遍历的应用场景,为后续构造树结构奠定基础。
62 10
监控局域网其他电脑:Go 语言迪杰斯特拉算法的高效应用
在信息化时代,监控局域网成为网络管理与安全防护的关键需求。本文探讨了迪杰斯特拉(Dijkstra)算法在监控局域网中的应用,通过计算最短路径优化数据传输和故障检测。文中提供了使用Go语言实现的代码例程,展示了如何高效地进行网络监控,确保局域网的稳定运行和数据安全。迪杰斯特拉算法能减少传输延迟和带宽消耗,及时发现并处理网络故障,适用于复杂网络环境下的管理和维护。
揭秘 Go 语言中空结构体的强大用法
Go 语言中的空结构体 `struct{}` 不包含任何字段,不占用内存空间。它在实际编程中有多种典型用法:1) 结合 map 实现集合(set)类型;2) 与 channel 搭配用于信号通知;3) 申请超大容量的 Slice 和 Array 以节省内存;4) 作为接口实现时明确表示不关注值。此外,需要注意的是,空结构体作为字段时可能会因内存对齐原因占用额外空间。建议将空结构体放在外层结构体的第一个字段以优化内存使用。
Go 语言中的 Sync.Map 详解:并发安全的 Map 实现
`sync.Map` 是 Go 语言中用于并发安全操作的 Map 实现,适用于读多写少的场景。它通过两个底层 Map(`read` 和 `dirty`)实现读写分离,提供高效的读性能。主要方法包括 `Store`、`Load`、`Delete` 等。在大量写入时性能可能下降,需谨慎选择使用场景。
企业监控软件中 Go 语言哈希表算法的应用研究与分析
在数字化时代,企业监控软件对企业的稳定运营至关重要。哈希表(散列表)作为高效的数据结构,广泛应用于企业监控中,如设备状态管理、数据分类和缓存机制。Go 语言中的 map 实现了哈希表,能快速处理海量监控数据,确保实时准确反映设备状态,提升系统性能,助力企业实现智能化管理。
45 3
阿里双十一背后的Go语言实践:百万QPS网关的设计与实现
解析阿里核心网关如何利用Go协程池、RingBuffer、零拷贝技术支撑亿级流量。 重点分享: ① 如何用gRPC拦截器实现熔断限流; ② Sync.Map在高并发读写中的取舍。
【Go语言入门100题】026 I Love GPLT (5 分) Go语言 | Golang
L1-026 I Love GPLT (5 分) Go语言|Golang 这道超级简单的题目没有任何输入。 你只需要把这句很重要的话 —— “I Love GPLT”——竖着输出就可以了。 所谓“竖着输出”,是指每个字符占一行(包括空格),即每行只能有1个字符和回车。
629 0
【Go语言入门100题】023 输出GPLT (20 分) Go语言 | Golang
L1-023 输出GPLT (20 分) Go语言|Golang 给定一个长度不超过10000的、仅由英文字母构成的字符串。请将字符重新调整顺序,按GPLTGPLT....这样的顺序输出,并忽略其它字符。当然,四种字符(不区分大小写)的个数不一定是一样多的,若某种字符已经输出完,则余下的字符仍按GPLT的顺序打印,直到所有字符都被输出。 下面给出甲、乙两人的酒量(最多能喝多少杯不倒)和划拳记录,请你判断两个人谁先倒。
197 0
基于 Go 语言的公司内网管理软件哈希表算法深度解析与研究
在数字化办公中,公司内网管理软件通过哈希表算法保障信息安全与高效管理。哈希表基于键值对存储和查找,如用户登录验证、设备信息管理和文件权限控制等场景,Go语言实现的哈希表能快速验证用户信息,提升管理效率,确保网络稳定运行。
39 0
eino — 基于go语言的大模型应用开发框架(二)
本文介绍了如何使用Eino框架实现一个基本的LLM(大语言模型)应用。Eino中的`ChatModel`接口提供了与不同大模型服务(如OpenAI、Ollama等)交互的统一方式,支持生成完整响应、流式响应和绑定工具等功能。`Generate`方法用于生成完整的模型响应,`Stream`方法以流式方式返回结果,`BindTools`方法为模型绑定工具。此外,还介绍了通过`Option`模式配置模型参数及模板功能,支持基于前端和用户自定义的角色及Prompt。目前主要聚焦于`ChatModel`的`Generate`方法,后续将继续深入学习。
416 7

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等