互斥锁与读写互斥锁的妙用

简介: 互斥锁与读写互斥锁的妙用

概述

在并发编程中,控制共享资源的访问是至关重要的。

Go 语言提供了两种主要的互斥锁,即 sync.Mutex(互斥锁)和 sync.RWMutex(读写互斥锁)。

本文将讨论这两种锁的使用方式、原理和适用场景,并通过实例代码演示它们在并发环境中的正确应用。


 

1. sync.Mutex(互斥锁)

1.1 互斥锁基本使用

互斥锁用于保护共享资源,确保同一时刻只有一个 Goroutine 可以访问。

下面是一个简单的示例


package main
import (  "fmt"  "sync"  "time")
var counter intvar mutex sync.Mutex
func main() {  var wg sync.WaitGroup
  for i := 0; i < 3; i++ {    wg.Add(1)    go incrementCounter(i, &wg)  }
  wg.Wait()  fmt.Printf("Final Counter: %d\n", counter)}
func incrementCounter(id int, wg *sync.WaitGroup) {  defer wg.Done()
  for i := 0; i < 5; i++ {    mutex.Lock()    counter++    fmt.Printf("Goroutine %d: Counter = %d\n", id, counter)    mutex.Unlock()    time.Sleep(100 * time.Millisecond)  }}

在这个用例中,使用 sync.Mutexcounter 变量进行了保护。

每次修改 counter 前,通过 Lock 方法锁定互斥锁,修改完成后使用 Unlock 方法释放锁。

这样可以确保多个 Goroutine 同时访问时不会发生数据竞争。

1.2 避免死锁

在使用互斥锁时,要特别注意避免死锁。

例如,如果一个 Goroutine 在锁定互斥锁后忘记释放锁,那么其他 Goroutine 将永远无法获取该锁。

下面是一个简单的死锁示例


package main
import (  "sync"  "time")
var mutex sync.Mutex
func main() {  var wg sync.WaitGroup
  wg.Add(1)  go deadlockExample(&wg)
  // 此处故意不调用 wg.Done(),导致死锁
  wg.Wait()}
func deadlockExample(wg *sync.WaitGroup) {  defer wg.Done()
  mutex.Lock()  defer mutex.Unlock()
  // 此处故意不释放锁,导致死锁  time.Sleep(2 * time.Second)}

在这个示例中,deadlockExample Goroutine 获取了互斥锁,

并在 defer 语句中注册了 Unlock 操作,但由于未调用 wg.Done()

导致 main Goroutine 永远无法结束,形成死锁。


 

2. sync.RWMutex(读写互斥锁)

2.1 读写互斥锁基本使用

读写互斥锁相比于互斥锁,更加灵活,允许多个 Goroutine 同时获取读锁,但只允许一个 Goroutine 获取写锁。

用一个简单的示例要演示


package main
import (  "fmt"  "sync"  "time")
var data map[string]stringvar rwMutex sync.RWMutex
func main() {  data = make(map[string]string)
  var wg sync.WaitGroup
  for i := 0; i < 3; i++ {    wg.Add(1)    go readData(i, &wg)  }
  for i := 0; i < 2; i++ {    wg.Add(1)    go writeData(i, &wg)  }
  wg.Wait()}
func readData(id int, wg *sync.WaitGroup) {  defer wg.Done()
  rwMutex.RLock()  defer rwMutex.RUnlock()
  fmt.Printf("Goroutine %d reading data: %v\n", id, data)    time.Sleep(500 * time.Millisecond)}
func writeData(id int, wg *sync.WaitGroup) {  defer wg.Done()
  rwMutex.Lock()  defer rwMutex.Unlock()
  key := fmt.Sprintf("key%d", id)  value := fmt.Sprintf("value%d", id)
  data[key] = value  fmt.Printf("Goroutine %d writing data: %v\n", id, data)  time.Sleep(500 * time.Millisecond)}

在这个示例中,用 sync.RWMutexdata 进行读写保护。

多个 Goroutine 可以同时获取读锁,但只有一个 Goroutine 能够获取写锁。

这样可以在读多写少的场景中提高并发性能。

2.2 避免写锁饥饿

写锁饥饿是指当有读锁持有时,写锁一直无法获取的情况。

要避免写锁饥饿,应该尽量减小读操作的临界区,避免长时间占用读锁。

下面是一个示例演示



package main
import (  "fmt"  "sync"  "time")
var data map[string]stringvar rwMutex sync.RWMutex
func main() {  data = make(map[string]string)
  var wg sync.WaitGroup
  for i := 0; i < 3; i++ {    wg.Add(1)    go readData(i, &wg)  }    //  等待读锁获取  time.Sleep(100 * time.Millisecond)
  wg.Add(1)  go writeData(1, &wg)
  wg.Wait()}
func readData(id int, wg *sync.WaitGroup) {  defer wg.Done()
  rwMutex.RLock()
  defer rwMutex.RUnlock()
  fmt.Printf("Goroutine %d reading data: %v\n", id, data)  time.Sleep(500 * time.Millisecond)}
func writeData(id int, wg *sync.WaitGroup) {  defer wg.Done()
  rwMutex.Lock()    defer rwMutex.Unlock()
  key := fmt.Sprintf("key%d", id)  value := fmt.Sprintf("value%d", id)
  data[key] = value  fmt.Printf("Goroutine %d writing data: %v\n", id, data)  time.Sleep(500 * time.Millisecond)}

在这个示例中,让三个 Goroutine 同时获取读锁,然后等待一段时间后再尝试获取写锁。

这样可以模拟写锁饥饿的情况。


 

总结

通过本文的详细讲解和实例演示,了解了 Go 语言中互斥锁和读写互斥锁的使用方式、原理以及注意事项。

在并发编程中,选择合适的锁机制是确保程序正确性和性能的关键一步。

通过灵活运用 sync.Mutexsync.RWMutex,可以更好地处理共享资源的并发访问,提高程序的健壮性和性能。

目录
相关文章
|
存储 Shell Linux
【Shell 命令集合 文件管理】Linux 读取命令 read命令使用指南
【Shell 命令集合 文件管理】Linux 读取命令 read命令使用指南
653 0
|
网络虚拟化
|
安全 网络协议 Unix
Web Security 之 OS command injection
Web Security 之 OS command injection
600 0
|
消息中间件 存储 NoSQL
Redis 竟然能用 List 实现消息队列
今天,码哥结合消息队列的特点一步步带大家分析使用 Redis 的 List 作为消息队列的实现原理,并分享如何把 SpringBoot 与 Redission 整合运用到项目中。
1152 0
Redis 竟然能用 List 实现消息队列
|
6月前
|
存储 人工智能 机器人
LangGraph 圣经:从0到1穿透 multi-agent多智能体 入门实战
LangGraph 圣经:从0到1穿透 multi-agent多智能体 入门实战
LangGraph 圣经:从0到1穿透 multi-agent多智能体 入门实战
|
存储 缓存 关系型数据库
MySQL为什么需要主键
本文介绍了MySQL中主键的重要性及最佳实践。主键用于唯一标识表中的每一行,其值必须唯一且不允许为空。主键有助于简化更新和删除操作,避免影响无关行。推荐使用与业务无关的自增ID作为Innodb表的主键,以优化存储结构、减少碎片并提高性能。此外,文章还提到不更新、不重用主键值以及避免使用可能变更的字段(如邮箱)作为主键的良好习惯。最后强调了紧凑索引结构对查询效率的关键作用。
392 0
MySQL为什么需要主键
|
JavaScript 前端开发 Java
编程语言中的常见Bug及解决方案
本文总结了主流编程语言中的常见Bug及其解决方案,涵盖JavaScript、Python、C/C++、Java和Go。例如,JavaScript中小数相加精度不准确,可通过`toFixed()`解决;Python 2中整数除法向下取整,改用浮点数除法;C/C++内存管理与缓冲区溢出,使用安全函数如`snprintf`;Java空指针异常,使用非空判断或`Optional`类;Go并发编程中的竞态条件,采用互斥锁或原子操作。了解这些Bug及解决方案有助于提高编程质量。
400 9
|
负载均衡 Dubbo Java
Dubbo 挂载到 Spring Cloud 注册中心
【2月更文挑战第12天】Dubbo 挂载到 Spring Cloud 注册中心
360 7
|
消息中间件 存储 缓存
为什么 Kafka 的吞吐量那么高?
为什么 Kafka 的吞吐量那么高?
548 2
|
存储 监控 Linux
stdbuf命令在Linux中的深度解析
`stdbuf`是Linux工具,用于控制命令的stdin、stdout和stderr的缓冲模式。它可以设置为无缓冲、行缓冲或块缓冲,以优化数据处理和实时性。例如,`stdbuf -o0 cmd`禁用cmd的输出缓冲,`-oL`则按行缓冲。在需要实时监控或高效处理大量数据时,选择合适的缓冲模式至关重要。注意,过度使用无缓冲可能影响性能,并非所有系统都支持`stdbuf`。

热门文章

最新文章