Go语言并发编程之互斥锁Mutex和读写锁RWMutex

目录
  • 一、互斥锁Mutex
    • 1、Mutex介绍
    • 2、Mutex使用实例
  • 二、读写锁RWMutex
    • 1、RWMutex介绍
    • 2、RWMutex使用实例

在并发编程中,多个Goroutine访问同一块内存资源时可能会出现竞态条件,我们需要在临界区中使用适当的同步操作来以避免竞态条件。Go 语言中提供了很多同步工具,本文将介绍互斥锁Mutex和读写锁RWMutex的使用方法。

一、互斥锁Mutex

1、Mutex介绍

Go 语言的同步工具主要由 sync 包提供,互斥锁 (Mutex) 与读写锁 (RWMutex) 就是sync 包中的方法。

互斥锁可以用来保护一个临界区,保证同一时刻只有一个 goroutine 处于该临界区内。主要包括锁定(Lock方法)和解锁(Unlock方法)两个操作,首先对进入临界区的goroutine进行锁定,离开时进行解锁。

使用互斥锁 (Mutex)时要注意以下几点:

  • 不要重复锁定互斥锁,否则会阻塞,也可能会导致死锁(deadlock);
  • 要对互斥锁进行解锁,这也是为了避免重复锁定;
  • 不要对未锁定或者已解锁的互斥锁解锁;
  • 不要在多个函数之间直接传递互斥锁,sync.Mutex类型属于值类型,将它传给一个函数时,会产生一个副本,在函数中对锁的操作不会影响原锁

总之,一个互斥锁只用来保护一个临界区,加锁后记得解锁,对于每一个锁定操作,都要有且只有一个对应的解锁操作,也就是加锁和解锁要成对出现,最保险的做法时使用 defer语句 解锁。

2、Mutex使用实例

下面的代码模拟取钱和存钱操作:

package main

import (
 "flag"
 "fmt"
 "sync"
)

var (
    mutex   sync.Mutex
    balance int
    protecting uint  // 是否加锁
    sign = make(chan struct{}, 10) //通道,用于等待所有goroutine
)

// 存钱
func deposit(value int) {
    defer func() {
        sign <- struct{}{}
    }()

    if protecting == 1 {
        mutex.Lock()
        defer mutex.Unlock()
    }

    fmt.Printf("余额: %d\n", balance)
    balance += value
    fmt.Printf("存 %d 后的余额: %d\n", value, balance)
    fmt.Println()

}

// 取钱
func withdraw(value int) {
    defer func() {
        sign <- struct{}{}
    }()

    if protecting == 1 {
        mutex.Lock()
        defer mutex.Unlock()
    }

    fmt.Printf("余额: %d\n", balance)
    balance -= value
    fmt.Printf("取 %d 后的余额: %d\n", value, balance)
    fmt.Println()

}

func main() {

    for i:=0; i < 5; i++ {
        go withdraw(500) // 取500
        go deposit(500)  // 存500
    }

    for i := 0; i < 10; i++ {
  <-sign
 }
    fmt.Printf("当前余额: %d\n", balance)
}

func init() {
    balance = 1000 // 初始账户余额为1000
    flag.UintVar(&protecting, "protecting", 0, "是否加锁,0表示不加锁,1表示加锁")
}

上面的代码中,使用了通道来让主 goroutine 等待其他 goroutine 运行结束,每个子goroutine在运行结束之前向通道发送一个元素,主 goroutine 在最后从这个通道接收元素,接收次数与子goroutine个数相同。接收完后就会退出主goroutine

代码使用协程实现多次(5次)对一个账户进行存钱和取钱的操作,先来看不加锁的情况:

余额: 1000
存 500 后的余额: 1500

余额: 1000
取 500 后的余额: 1000

余额: 1000
存 500 后的余额: 1500

余额: 1000
取 500 后的余额: 1000

余额: 1000
存 500 后的余额: 1500

余额: 1000
取 500 后的余额: 1000

余额: 1000
取 500 后的余额: 500

余额: 1000
存 500 后的余额: 1000

余额: 1000
取 500 后的余额: 500

余额: 1000
存 500 后的余额: 1000

当前余额: 1000

可以看到出现了混乱,比如第二次1000的余额取500后还是1000,这种对同一资源的竞争出现了竞态条件(Race Condition)。

下面来看加锁的执行结果:

余额: 1000
取 500 后的余额: 500

余额: 500
存 500 后的余额: 1000

余额: 1000
取 500 后的余额: 500

余额: 500
存 500 后的余额: 1000

余额: 1000
取 500 后的余额: 500

余额: 500
存 500 后的余额: 1000

余额: 1000
存 500 后的余额: 1500

余额: 1500
取 500 后的余额: 1000

余额: 1000
取 500 后的余额: 500

余额: 500
存 500 后的余额: 1000

当前余额: 1000

加锁后就正常了。

下面介绍更细化的互斥锁:读/写互斥锁RWMutex。

二、读写锁RWMutex

1、RWMutex介绍

读/写互斥锁RWMutex包含了读锁和写锁,分别对共享资源的“读操作”和“写操作”进行保护。sync.RWMutex类型中的Lock方法和Unlock方法分别用于对写锁进行锁定和解锁,而它的RLock方法和RUnlock方法则分别用于对读锁进行锁定和解锁。

有了互斥锁Mutex,为什么还需要读写锁呢?因为在很多并发操作中,并发读取占比很大,写操作相对较少,读写锁可以并发读取,这样可以提供服务性能。读写锁具有以下特征:

读写锁 读锁 写锁
读锁 Yes No
写锁 No No

也就是说,

  • 如果某个共享资源受到读锁和写锁保护时,其它goroutine不能进行写操作。换句话说就是读写操作和写写操作不能并行执行,也就是读写互斥;
  • 受读锁保护时,可以同时进行多个读操作。

在使用读写锁时,还需要注意:

  • 不要对未锁定的读写锁解锁;
  • 对读锁不能使用写锁解锁
  • 对写锁不能使用读锁解锁

2、RWMutex使用实例

改写前面的取钱和存钱操作,添加查询余额的方法:

package main

import (
 "fmt"
 "sync"
)

// account 代表计数器。
type account struct {
 num uint         // 操作次数
 balance int   // 余额
 rwMu  *sync.RWMutex // 读写锁
}

var sign = make(chan struct{}, 15) //通道,用于等待所有goroutine

// 查看余额:使用读锁
func (c *account) check() {
 defer func() {
        sign <- struct{}{}
    }()
 c.rwMu.RLock()
 defer c.rwMu.RUnlock()
 fmt.Printf("%d 次操作后的余额: %d\n", c.num, c.balance)
}

// 存钱:写锁
func (c *account) deposit(value int) {
 defer func() {
        sign <- struct{}{}
    }()
    c.rwMu.Lock()
 defer c.rwMu.Unlock() 

 fmt.Printf("余额: %d\n", c.balance)
 c.num += 1
    c.balance += value
    fmt.Printf("存 %d 后的余额: %d\n", value, c.balance)
    fmt.Println()
}

// 取钱:写锁
func (c *account) withdraw(value int) {
    defer func() {
        sign <- struct{}{}
    }()
 c.rwMu.Lock()
 defer c.rwMu.Unlock()
 fmt.Printf("余额: %d\n", c.balance)
 c.num += 1
    c.balance -= value
 fmt.Printf("取 %d 后的余额: %d\n", value, c.balance)
    fmt.Println()
}

func main() {
 c := account{0, 1000, new(sync.RWMutex)}

 for i:=0; i < 5; i++ {
        go c.withdraw(500) // 取500
        go c.deposit(500)  // 存500
  go c.check()
    }

    for i := 0; i < 15; i++ {
  <-sign
 }
 fmt.Printf("%d 次操作后的余额: %d\n", c.num, c.balance)

}

执行结果:

余额: 1000
取 500 后的余额: 500

1 次操作后的余额: 500
1 次操作后的余额: 500
1 次操作后的余额: 500
1 次操作后的余额: 500
1 次操作后的余额: 500
余额: 500
存 500 后的余额: 1000

余额: 1000
取 500 后的余额: 500

余额: 500
存 500 后的余额: 1000

余额: 1000
存 500 后的余额: 1500

余额: 1500
取 500 后的余额: 1000

余额: 1000
取 500 后的余额: 500

余额: 500
存 500 后的余额: 1000

余额: 1000
取 500 后的余额: 500

余额: 500
存 500 后的余额: 1000

10 次操作后的余额: 1000

读写锁和互斥锁的不同之处在于读写锁把对共享资源的读操作和写操作分开了,可以实现更复杂的访问控制。

总结:

读写锁也是一种互斥锁,它是互斥锁的扩展。在使用时需要注意:

  • 加锁后一定要解锁
  • 不要重复加锁或者解锁
  • 不解锁未锁定的锁
  • 不要传递互斥锁

到此这篇关于Go语言并发编程之互斥锁Mutex和读写锁RWMutex的文章就介绍到这了,更多相关Go语言 Mutex RWMutex内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • GO语言并发编程之互斥锁、读写锁详解

    在本节,我们对Go语言所提供的与锁有关的API进行说明.这包括了互斥锁和读写锁.我们在第6章描述过互斥锁,但却没有提到过读写锁.这两种锁对于传统的并发程序来说都是非常常用和重要的. 一.互斥锁 互斥锁是传统的并发程序对共享资源进行访问控制的主要手段.它由标准库代码包sync中的Mutex结构体类型代表.sync.Mutex类型(确切地说,是*sync.Mutex类型)只有两个公开方法--Lock和Unlock.顾名思义,前者被用于锁定当前的互斥量,而后者则被用来对当前的互斥量进行解锁. 类型sy

  • Go语言并发编程之互斥锁Mutex和读写锁RWMutex

    目录 一.互斥锁Mutex 1.Mutex介绍 2.Mutex使用实例 二.读写锁RWMutex 1.RWMutex介绍 2.RWMutex使用实例 在并发编程中,多个Goroutine访问同一块内存资源时可能会出现竞态条件,我们需要在临界区中使用适当的同步操作来以避免竞态条件.Go 语言中提供了很多同步工具,本文将介绍互斥锁Mutex和读写锁RWMutex的使用方法. 一.互斥锁Mutex 1.Mutex介绍 Go 语言的同步工具主要由 sync 包提供,互斥锁 (Mutex) 与读写锁 (R

  • GoLang中的互斥锁Mutex和读写锁RWMutex使用教程

    目录 一.竞态条件与临界区和同步工具 (1)竞态条件 (2)临界区 (3)同步工具 二.互斥量 三.使用互斥锁的注意事项 (1)使用互斥锁的注意事项 (2)使用defer语句解锁 (3)sync.Mutex是值类型 四.读写锁与互斥锁的异同 (1)读/写互斥锁 (2)读写锁规则 (3)解锁读写锁 一.竞态条件与临界区和同步工具 (1)竞态条件 一旦数据被多个线程共享,那么就会产生冲突和争用的情况,这种情况被称为竞态条件.这往往会破坏数据的一致性. 同步的用途有两个,一个是避免多线程在同一时刻操作

  • GO语言协程互斥锁Mutex和读写锁RWMutex用法实例详解

    sync.Mutex Go中使用sync.Mutex类型实现mutex(排他锁.互斥锁).在源代码的sync/mutex.go文件中,有如下定义: // A Mutex is a mutual exclusion lock. // The zero value for a Mutex is an unlocked mutex. // // A Mutex must not be copied after first use. type Mutex struct { state int32 sem

  • java并发编程中ReentrantLock可重入读写锁

    目录 一.ReentrantLock可重入锁 二.ReentrantReadWriteLock读写锁 三.读锁之间不互斥 一.ReentrantLock可重入锁 可重入锁ReentrantLock 是一个互斥锁,即同一时间只有一个线程能够获取锁定资源,执行锁定范围内的代码.这一点与synchronized 关键字十分相似.其基本用法代码如下: Lock lock = new ReentrantLock(); //实例化锁 //lock.lock(); //上锁 boolean locked =

  • python并发编程多进程 互斥锁原理解析

    运行多进程 每个子进程的内存空间是互相隔离的 进程之间数据不能共享的 互斥锁 但是进程之间都是运行在一个操作系统上,进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端, 是可以的,而共享带来的是竞争,竞争带来的结果就是错乱 #并发运行,效率高,但竞争同一打印终端,带来了打印错乱 from multiprocessing import Process import time def task(name): print("%s 1" % name) time.

  • Go语言并发编程 互斥锁详情

    目录 1.互斥锁Mutex 1.1 Mutex介绍 1.2 Mutex使用实例 2.读写锁RWMutex 2.1 RWMutex介绍 2.2 RWMutex使用实例 1.互斥锁Mutex 1.1 Mutex介绍 Go 语言的同步工具主要由 sync 包提供,互斥锁 (Mutex) 与读写锁 (RWMutex) 就是sync 包中的方法. 互斥锁可以用来保护一个临界区,保证同一时刻只有一个 goroutine 处于该临界区内.主要包括锁定(Lock方法)和解锁(Unlock方法)两个操作,首先对进

  • Go语言读写锁RWMutex的源码分析

    目录 前言 RWMutex 总览 深入源码 数据结构 RLock() RUnlock() Lock() Unlock() 常见问题 实战一下 前言 在前面两篇文章中 初见 Go Mutex .Go Mutex 源码详解,我们学习了 Go语言 中的 Mutex,它是一把互斥锁,每次只允许一个 goroutine 进入临界区,可以保证临界区资源的状态正确性.但是有的情况下,并不是所有 goroutine 都会修改临界区状态,可能只是读取临界区的数据,如果此时还是需要每个 goroutine 拿到锁依

  • 一文掌握Go语言并发编程必备的Mutex互斥锁

    目录 1. Mutex 互斥锁的基本概念 2. Mutex 互斥锁的基本用法 3. Mutex 互斥锁的底层实现 3.1 等待队列 3.2 锁状态 4. Mutex 互斥锁的注意事项 4.1 不要将 Mutex 作为函数或方法的参数传递 4.2 不要在获取 Mutex 的锁时阻塞太久 4.3 不要重复释放 Mutex 的锁 4.4 不要在锁内部执行阻塞或耗时操作 5. 总结 在并发编程中,我们需要处理多个线程同时对共享资源的访问问题.如果不加控制地同时访问共享资源,就会导致竞争条件(Race C

  • Go语言并发编程 sync.Once

    sync.Once用于保证某个动作只被执行一次,可用于单例模式中,比如初始化配置.我们知道init()函数也只会执行一次,不过它是在main()函数之前执行,如果想要在代码执行过程中只运行某个动作一次,可以使用sync.Once,下面来介绍一下它的使用方法. 先来看下面的代码: package main import ( "fmt" "sync" ) func main() { var num = 6 var once sync.Once add_one := fu

随机推荐