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 持有。我们可以利用读写锁来锁定某个操作以防止其他例程/线程在处理它时更改值,防止程序出现不可预测的错误。最后,可以利用读写锁弥补互斥锁的缺陷,用来加快程序的读操作,减少程序的运行时间。

相关文章
|
10天前
|
算法 安全 Go
Go语言中的加密和解密是如何实现的?
Go语言通过标准库中的`crypto`包提供丰富的加密和解密功能,包括对称加密(如AES)、非对称加密(如RSA、ECDSA)及散列函数(如SHA256)。`encoding/base64`包则用于Base64编码与解码。开发者可根据需求选择合适的算法和密钥,使用这些包进行加密操作。示例代码展示了如何使用`crypto/aes`包实现对称加密。加密和解密操作涉及敏感数据处理,需格外注意安全性。
33 14
|
10天前
|
Go 数据库
Go语言中的包(package)是如何组织的?
在Go语言中,包是代码组织和管理的基本单元,用于集合相关函数、类型和变量,便于复用和维护。包通过目录结构、文件命名、初始化函数(`init`)及导出规则来管理命名空间和依赖关系。合理的包组织能提高代码的可读性、可维护性和可复用性,减少耦合度。例如,`stringutils`包提供字符串处理函数,主程序导入使用这些函数,使代码结构清晰易懂。
49 11
|
10天前
|
存储 安全 Go
Go语言中的map数据结构是如何实现的?
Go 语言中的 `map` 是基于哈希表实现的键值对数据结构,支持快速查找、插入和删除操作。其原理涉及哈希函数、桶(Bucket)、动态扩容和哈希冲突处理等关键机制,平均时间复杂度为 O(1)。为了确保线程安全,Go 提供了 `sync.Map` 类型,通过分段锁实现并发访问的安全性。示例代码展示了如何使用自定义结构体和切片模拟 `map` 功能,以及如何使用 `sync.Map` 进行线程安全的操作。
|
14天前
|
监控 安全 算法
深度剖析核心科技:Go 语言赋能局域网管理监控软件进阶之旅
在局域网管理监控中,跳表作为一种高效的数据结构,能显著提升流量索引和查询效率。基于Go语言的跳表实现,通过随机化索引层生成、插入和搜索功能,在高并发场景下展现卓越性能。跳表将查询时间复杂度优化至O(log n),助力实时监控异常流量,保障网络安全与稳定。示例代码展示了其在实际应用中的精妙之处。
36 9
|
24天前
|
算法 安全 Go
Go 语言中实现 RSA 加解密、签名验证算法
随着互联网的发展,安全需求日益增长。非对称加密算法RSA成为密码学中的重要代表。本文介绍如何使用Go语言和[forgoer/openssl](https://github.com/forgoer/openssl)库简化RSA加解密操作,包括秘钥生成、加解密及签名验证。该库还支持AES、DES等常用算法,安装简便,代码示例清晰易懂。
58 12
|
8月前
|
开发框架 安全 中间件
Go语言开发小技巧&易错点100例(十二)
Go语言开发小技巧&易错点100例(十二)
87 1
|
1月前
|
开发框架 Go 计算机视觉
纯Go语言开发人脸检测、瞳孔/眼睛定位与面部特征检测插件-助力GoFly快速开发框架
开发纯go插件的原因是因为目前 Go 生态系统中几乎所有现有的人脸检测解决方案都是纯粹绑定到一些 C/C++ 库,如 OpenCV 或 dlib,但通过 cgo 调用 C 程序会引入巨大的延迟,并在性能方面产生显著的权衡。此外,在许多情况下,在各种平台上安装 OpenCV 是很麻烦的。使用纯Go开发的插件不仅在开发时方便,在项目部署和项目维护也能省很多时间精力。
|
2月前
|
Go 数据安全/隐私保护 开发者
Go语言开发
【10月更文挑战第26天】Go语言开发
50 3
|
2月前
|
Java 程序员 Go
Go语言的开发
【10月更文挑战第25天】Go语言的开发
44 3
|
5月前
|
JSON 中间件 Go
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
本文详细介绍了如何在Go项目中集成并配置Zap日志库。首先通过`go get -u go.uber.org/zap`命令安装Zap,接着展示了`Logger`与`Sugared Logger`两种日志记录器的基本用法。随后深入探讨了Zap的高级配置,包括如何将日志输出至文件、调整时间格式、记录调用者信息以及日志分割等。最后,文章演示了如何在gin框架中集成Zap,通过自定义中间件实现了日志记录和异常恢复功能。通过这些步骤,读者可以掌握Zap在实际项目中的应用与定制方法
197 1
go语言后端开发学习(四) —— 在go项目中使用Zap日志库