Golang之路---04 并发编程——互斥锁和读写锁

本文介绍了Golang中互斥锁(Mutex)和读写锁(RWMutex)在并发编程中的应用,通过实际例子解释了它们如何防止数据不准确并处理并发操作。

互斥锁和读写锁

面对并发问题,我们始终应该优先考虑使用信道,如果通过信道解决不了的,不得不使用共享内存来实现并发编程的,那 Golang 中的锁机制,就是你绕不过的知识点了。
在 Golang 里有专门的方法来实现锁,还是上一节里介绍的 sync 包。

这个包有两个很重要的锁类型

一个叫 Mutex, 利用它可以实现互斥锁。

一个叫 RWMutex,利用它可以实现读写锁。

互斥锁 :Mutex

使用互斥锁(Mutex,全称 mutual exclusion)是为了来保护一个资源不会因为并发操作而引起冲突导致数据不准确。
举个例子,就像下面这段代码,我开启了三个协程,每个协程分别往 count 这个变量加1000次 1,理论上看,最终的 count 值应试为 3000


func add(count *int, wg *sync.WaitGroup){
    for i:= 0; i < 1000; i++{
        *count = *count + 1
    }
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    count := 0
    wg.Add(3)
    go add(&count, &wg)
    go add(&count, &wg)
    go add(&count, &wg)

    wg.Wait()
    /* 
    count的值为: 2078
    count的值为: 2516
    count的值为: 2079
    */
    fmt.Println("count的值为:",count)
}

可运行多次的结果,都不相同

   count的值为: 2078
   count的值为: 2516
   count的值为: 2079

原因就在于这三个协程在执行时,先读取 count 再更新 count 的值,而这个过程并不具备原子性,所以导致了数据的不准确。

解决这个问题的方法,就是给 add 这个函数加上 Mutex 互斥锁,要求同一时刻,仅能有一个协程能对 count 操作。

在写代码前,先了解一下 Mutex 锁的两种定义方法

// 第一种
var lock *sync.Mutex
lock = new(sync.Mutex)

// 第二种
lock := &sync.Mutex{}

利用互斥锁修改上面的代码,得:


func add(count *int, wg *sync.WaitGroup, lock *sync.Mutex){
    for i:= 0; i < 1000; i++{
        lock.Lock()
        *count = *count + 1
        lock.Unlock()
    }
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    lock := &sync.Mutex{}
    count := 0
    wg.Add(3)
    go add(&count, &wg, lock)
    go add(&count, &wg, lock)
    go add(&count, &wg, lock)

    wg.Wait()
    
    fmt.Println("count的值为:",count)
}

此时,不管你执行多少次,输出都只有一个结果

count 的值为: 3000

使用 Mutex 锁的注意事项:

  • 同一协程里,不要在尚未解锁时再次使加锁

  • 同一协程里,不要对已解锁的锁再次解锁

  • 加了锁后,别忘了解锁,必要时使用 defer 语句

读写锁:RWMutex

RWMutex,它将程序对资源的访问分为读操作和写操作
为了保证数据的安全,它规定了当有人还在读取数据(即读锁占用)时,不允计有人更新这个数据(即写锁会阻塞)

为了保证程序的效率,多个人(线程)读取数据(拥有读锁)时,互不影响不会造成阻塞,它不会像 Mutex 那样只允许有一个人(线程)读取同一个数据。

读锁允许多个线程同时获得,因为读操作本身是线程安全的。
而写锁则是互斥锁,不允许多个线程同时获得写锁,并且写操作和读操作也是互斥的。
读写锁的特点是:读读不互斥、读写互斥、写写互斥

定义一个 RWMuteux 锁,有两种方法

// 第一种
var lock *sync.RWMutex
lock = new(sync.RWMutex)

// 第二种
lock := &sync.RWMutex{}

RWMutex 里提供了两种锁,每种锁分别对应两个方法,为了避免死锁,两个方法应成对出现,必要时请使用 defer。

读锁:调用 RLock 方法开启锁,调用 RUnlock 释放锁

写锁:调用 Lock 方法开启锁,调用 Unlock 释放锁(和 Mutex类似)

基本遵循原则:
  • 写锁定情况下,对读写锁进行读锁定或者写锁定,都将阻塞;而且读锁与写锁之间是互斥的;

  • 读锁定情况下,对读写锁进行写锁定,将阻塞;加读锁时不会阻塞;

  • 对未被写锁定的读写锁进行写解锁,会引发 Panic;

  • 对未被读锁定的读写锁进行读解锁的时候也会引发 Panic;

  • 写解锁在进行的同时会试图唤醒所有因进行读锁定而被阻塞的 goroutine;

  • 读解锁在进行的时候则会试图唤醒一个因进行写锁定而被阻塞的 goroutine。


func main() {
    lock := &sync.RWMutex{}
    lock.Lock()

    for i := 0;i < 4;i++{
        go func (i int)  {
            fmt.Printf("第 %d 个协程准备开始... \n",i)
            lock.RLock()
            fmt.Printf("第 %d 个协程获得读锁,sleep 1s后,释放锁\n",i)
            time.Sleep(time.Second)
            lock.RUnlock();
        }(i)
    }

    time.Sleep(time.Second * 2)

    fmt.Println("准备释放写锁,读锁不再阻塞")
    //写锁一释放,读锁就自由了
    lock.Unlock()

    lock.Lock()
    fmt.Println("程序退出...")
    lock.Unlock()
}

在这里插入图片描述

Golang互斥锁读写锁存在多方面区别: - **并发控制机制**:互斥锁是完全互斥的,使用互斥锁能够保证同一时间有且只有一个 goroutine 进入临界区,其他的 goroutine 则在等待;当互斥锁释放后,等待的 goroutine 才可以获取进入临界区,多个 goroutine 同时等待一个时,唤醒的策略是随机的。而读写锁允许多个协程同时读取某个共享资源,但在写入时必须互斥,只能有一个协程进行写操作 [^1][^3]。 - **适用场景**:互斥锁适用于对共享资源的读写操作都需要严格互斥的场景。读写锁则更适用于读多写少的场景,当并发读取一个资源且涉及资源修改时无需加,这种场景使用读写锁是更好的选择 [^1][^3]。 - **性能表现**:读写互斥锁在高并发读的场景下可以提高并发性能,但在高并发写的场景下仍然存在性能瓶颈。有人怀疑 golang 中 `sync.Mutex` 的 `Lock` `Unlock` 在底层实现的时候要比 `sync.RWMutex` 的 `RLock` `RUnlock` 性能要好 [^1][^2]。 以下是使用互斥锁读写锁的示例代码: ```go package main import ( "fmt" "math/rand" "sync" "time" ) var count int var mutex sync.RWMutex func main() { fmt.Println("start study golang...... ") for i := 0; i < 10; i++ { go read(i + 1) } for i := 0; i < 10; i++ { go write(i + 1) } time.Sleep(time.Second * 5) } func write(n int) { rand.Seed(time.Now().UnixNano()) fmt.Errorf("写 goroutine %d 正在写数据...\n", n) mutex.Lock() num := rand.Intn(500) count = num fmt.Printf("写 goroutine %d 写数据结束,写入新值 %d\n", n, num) mutex.Unlock() } func read(n int) { mutex.RLock() fmt.Printf("读 goroutine %d 正在读取数据...\n", n) num := count fmt.Printf("读 goroutine %d 读取数据结束,读到 %d\n", n, num) mutex.RUnlock() } ```
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值