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

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

前言

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


通过使用 sync.RWMutex,我们的程序变得更加高效。

什么是读者-写者问题

先来了解读者-写者问题(Readers–writers problem)的背景。最基本的读者-写者问题首先由 Courtois 等人提出并解决。


读者-写者问题描述了计算机并发处理读写数据遇到的问题,如何保证数据完整性、一致性。解决读者-写者问题需保证对于一份资源操作满足以下下条件:

  • 读写互斥
  • 写写互斥
  • 允许多个读者同时读取

解决读者-写者问题,可以采用读者优先(readers-preference)方案或者写者优先(writers-preference)方案。

  • 读者优先(readers-preference):读者优先是读操作优先于写操作,即使写操作提出申请资源,但只要还有读者在读取操作,就还允许其他读者继续读取操作,直到所有读者结束读取,才开始写。读优先可以提供很高的并发处理性能,但是在频繁读取的系统中,会长时间写阻塞,导致写饥饿。
  • 写者优先(writers-preference):写者优先是写操作优先于读操作,如果有写者提出申请资源,在申请之前已经开始读取操作的可以继续执行读取,但是如果再有读者申请读取操作,则不能够读取,只有在所有的写者写完之后才可以读取。写者优先解决了读者优先造成写饥饿的问题。但是若在频繁写入的系统中,会长时间读阻塞,导致读饥饿。

RWMutex 设计采用写者优先方法,保证写操作优先处理。


回顾一下互斥锁的案例

多次单笔存款

假设你有一个银行账户,那你既可以进行存钱,也可以查询余额的操作。

package main
import "fmt"
type Account struct {
  name    string
  balance float64
}
//
func (a *Account) Deposit(amount float64) {
  a.balance += amount
}
func (a *Account) Balance() float64 {
  return a.balance
}
func main() {
  user := &Account{"xiaoW", 0}
  user.Deposit(10000)
  user.Deposit(200)
  user.Deposit(2022)
  fmt.Printf("%s's account balance has %.2f $.", user.name, user.Balance())
}

执行该代码,进行三笔存款,我们可以看到输出的账户余额为 12222.00 $:

$ go run main.go     
xiaoW's account balance has 12222.00 $.


同时多次存款

但如果我们进行同时存款呢?即使用 goroutine 来生成三个线程来模拟同时存款的操作。然后利用sync.WaitGroup 去等待所有 goroutine 执行完毕,打印最后的余额:

package main
import (
  "fmt"
  "sync"
)
type Account struct {
  name    string
  balance float64
}
func (a *Account) Deposit(amount float64) {
  a.balance += amount
}
func (a *Account) Balance() float64 {
  return a.balance
}
func main() {
  var wg sync.WaitGroup
  user := &Account{"xiaoW", 0}
  wg.Add(3)
  go func() {
    user.Deposit(10000)
    wg.Done()
  }()
  go func() {
    user.Deposit(200)
    wg.Done()
  }()
  go func() {
    user.Deposit(2022)
    wg.Done()
  }()
  wg.Wait()
  fmt.Printf("%s's account banlance has %.2f $.", user.name, user.Balance())
}


同时执行 3 次是没问题的,但如果执行 1000 次呢?

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

我们多次运行该程序,发现每次运行结果都不一样。

$  go run main.go
xiaoW's account banlance has 0.00 $.                                                                                                         
$  go run main.go
xiaoW's account banlance has 886000.00 $.                                                                                                     
$  go run main.go
xiaoW's account banlance has 2000.00 $.

正常的结果应该为 1000 * 1000 = 1000000.00 的余额才对,运行很多次的情况下才能看到一次正常的结果。

xiaoW's account banlance has 1000000.00 $.


使用 -race 参数来查看数据竞争


我们可以利用 -race 参数来查看我们的代码是否有竞争:

$  go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c00000e040 by goroutine 7:
  main.(*Account).Deposit()
      /home/wade/GoProjects/Go RWMutex/v2/main.go:15 +0x48
  main.main.func1()
      /home/wade/GoProjects/Go RWMutex/v2/main.go:31 +0x36
Previous write at 0x00c00000e040 by goroutine 45:
  main.(*Account).Deposit()
      /home/wade/GoProjects/Go RWMutex/v2/main.go:15 +0x6e
  main.main.func1()
      /home/wade/GoProjects/Go RWMutex/v2/main.go:31 +0x36
Goroutine 7 (running) created at:
  main.main()
      /home/wade/GoProjects/Go RWMutex/v2/main.go:30 +0x144
Goroutine 45 (finished) created at:
  main.main()
      /home/wade/GoProjects/Go RWMutex/v2/main.go:30 +0x144
==================
xiaoW's account banlance has 996000.00 $.Found 1 data race(s)
exit status 66

我们可以看到了发生了 goroutine 的线程竞争,goroutine 7 在读的时候,goroutine 45 在写,最终导致了读写不一致,所以最终的余额也都不符合我们的预期。

image.png


互斥锁:sync.Mutex

对于上述发生的线程竞争问题,我们就可以使用互斥锁来解决,即同一时间只能有一个 goroutine 能够处理该函数。代码改正如下:

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.Wait()
  fmt.Printf("%s's account banlance has %.2f $.", user.name, user.Balance())
}

此时,我们再运行 3 次 go run -race main.go,得到统一的结果:

$  go run -race main.go
xiaoW's account banlance has 1000000.00 $.                                                                                                  
$  go run -race main.go
xiaoW's account banlance has 1000000.00 $.                                                                                                   
$  go run -race main.go
xiaoW's account banlance has 1000000.00 $.


相关文章
|
4天前
|
数据采集 存储 Go
使用Go语言和chromedp库下载Instagram图片:简易指南
Go语言爬虫示例使用chromedp库下载Instagram图片,关键步骤包括设置代理IP、创建带代理的浏览器上下文及执行任务,如导航至用户页面、截图并存储图片。代码中新增`analyzeAndStoreImage`函数对图片进行分析和分类后存储。注意Instagram的反爬策略可能需要代码适时调整。
使用Go语言和chromedp库下载Instagram图片:简易指南
|
1天前
|
安全 Go 开发者
Golang深入浅出之-Go语言并发编程面试:Goroutine简介与创建
【4月更文挑战第22天】Go语言的Goroutine是其并发模型的核心,是一种轻量级线程,能低成本创建和销毁,支持并发和并行执行。创建Goroutine使用`go`关键字,如`go sayHello(&quot;Alice&quot;)`。常见问题包括忘记使用`go`关键字、不正确处理通道同步和关闭、以及Goroutine泄漏。解决方法包括确保使用`go`启动函数、在发送完数据后关闭通道、设置Goroutine退出条件。理解并掌握这些能帮助开发者编写高效、安全的并发程序。
10 1
|
1天前
|
人工智能 Go 调度
掌握Go并发:Go语言并发编程深度解析
掌握Go并发:Go语言并发编程深度解析
|
1天前
|
SQL 关系型数据库 MySQL
Golang数据库编程详解 | 深入浅出Go语言原生数据库编程
Golang数据库编程详解 | 深入浅出Go语言原生数据库编程
|
2天前
|
Go 开发者
Golang深入浅出之-Go语言流程控制:if、switch、for循环详解
【4月更文挑战第21天】本文介绍了Go语言中的流程控制语句,包括`if`、`switch`和`for`循环。`if`语句支持简洁的语法和初始化语句,但需注意比较运算符的使用。`switch`语句提供多分支匹配,可省略`break`,同时支持不带表达式的形式。`for`循环有多种形式,如基本循环和`for-range`遍历,遍历时修改原集合可能导致未定义行为。理解并避免易错点能提高代码质量和稳定性。通过实践代码示例,可以更好地掌握Go语言的流程控制。
11 3
Golang深入浅出之-Go语言流程控制:if、switch、for循环详解
|
2天前
|
Go
Golang深入浅出之-Go语言函数基础:定义、调用与多返回值
【4月更文挑战第21天】Go语言函数是代码组织的基本单元,用于封装可重用逻辑。本文介绍了函数定义(包括基本形式、命名、参数列表和多返回值)、调用以及匿名函数与闭包。在函数定义时,注意参数命名和注释,避免参数顺序混淆。在调用时,要检查并处理多返回值中的错误。理解闭包原理,小心处理外部变量引用,以提升代码质量和可维护性。通过实践和示例,能更好地掌握Go语言函数。
16 1
Golang深入浅出之-Go语言函数基础:定义、调用与多返回值
|
3天前
|
程序员 Go API
【Go语言快速上手(二)】 分支与循环&函数讲解
【Go语言快速上手(二)】 分支与循环&函数讲解
|
3天前
|
Go
Golang深入浅出之-Go语言基础语法:变量声明与赋值
【4月更文挑战第20天】本文介绍了Go语言中变量声明与赋值的基础知识,包括使用`var`关键字和简短声明`:=`的方式,以及多变量声明与赋值。强调了变量作用域、遮蔽、初始化与零值的重要性,并提醒读者注意类型推断时的一致性。了解这些概念有助于避免常见错误,提高编程技能和面试表现。
18 0
|
3天前
|
编译器 Go 开发者
Go语言入门|包、关键字和标识符
Go语言入门|包、关键字和标识符
22 0
|
17天前
|
Go
go语言中的数据类型
go语言中的数据类型
13 0