August Rush

一个还在努力成长的小火汁!

游龙当归海,海不迎我自来也。

We create our own demons.

You can reach me at augustrush0923@gmail.com
Go语言基础-锁机制
发布:2024年01月22日 | 作者:augustrush | 阅读量: 1406

锁机制

Go语言中,信道的地位非常高,它是first class级别的,面对并发问题,我们始终应该优先考虑使用信道,如果通过信道解决不了的,不得不使用共享内存来实现并发编程的,那Golang中的锁机制,就是你绕不过的知识点了。

当多个goroutine同时操作一个资源的时候,可能会造成资源的竞争。下面通过一段代码来展示这个问题:

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup
var x int

func main() {
    // 开启两个协程来对全局变量x进行操作
    wg.Add(2)
    go func() {
        defer wg.Done()
        for i := 0; i < 10000; i++ {
            increase(&x)
        }
    }()

    go func() {
        defer wg.Done()
        for i := 0; i < 10000; i++ {
            increase(&x)
        }
    }()
    wg.Wait()
    fmt.Println(x)
}

func increase(n *int) {
    *n = *n + 1
}

当多次执行上述代码,得到的结果每次都大相径庭。12886,12258, 11514...

在上面的示例代码片中,我们开启了两个goroutine分别执行increase函数,这两个goroutine在访问和修改全局的x变量时就会存在数据竞争,某个goroutine中对全局变量x的修改可能会覆盖掉另一个goroutine中的操作,所以导致最后的结果与预期不符。

互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同一时间只有一个goroutine可以访问共享资源。Go语言中使用sync包中提供的Mutex类型来实现互斥锁。

sync/Mutex提供两个方法:

  • Lock(): 获取互斥锁
  • Unlock(): 释放互斥锁
package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup
var mutex sync.Mutex

var x int

func main() {
    wg.Add(2)
    go func() { // 多个协程对同一个全局变量进行更改
        defer wg.Done()
        for i := 0; i < 10000; i++ {
            mutex.Lock()
            increase(&x)
            mutex.Unlock()
        }
    }()

    go func() { // 多个协程对同一个全局变量进行更改
        defer wg.Done()
        for i := 0; i < 10000; i++ {
            mutex.Lock()
            increase(&x)
            mutex.Unlock()
        }
    }()
    // 问题: 会造成资源竞争,导致得不到想要的数据
    // 解决方法:给要操作的变量进行加锁操作,但这会导致程序编程单协程,因为加锁时会阻塞,其他协程也无法运行。
    wg.Wait()
    fmt.Println(x)
}

func increase(n *int) {
    *n = *n + 1
}

将上面的代码编译后多次执行,每一次都会得到预期中的结果——20000。

互斥锁会保证每个同一时间有且只有一个goroutine可以获取到资源,其他goroutine会处于阻塞状态,等待那个获取到资源重新释放后,才会重新竞争获取锁。

使用Mutex锁虽然很简单,但仍然有几点需要注意:

  • 同一协程里,不要在尚未解锁时再次使加锁
  • 同一协程里,不要对已解锁的锁再次解锁
  • 加了锁后,别忘了解锁,必要时使用defer语句

读写互斥锁

Mutex是最简单的一种锁类型,他提供了一个傻瓜式的操作,加锁解锁加锁解锁,让你不需要再考虑其他的。 简单同时意味着在某些特殊情况下有可能会造成时间上的浪费,导致程序性能低下。

很多场景是读多写少的,当我们并发的去读取一个资源而不涉及资源修改的时候是没有必要加互斥锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。

sync/RWMutex提供五个方法:

  • Lock(): 获取写锁
  • Unlock(): 释放写锁
  • RLock(): 获取读锁
  • RUnlock(): 释放读锁
  • RLocker(): 返回一个实现Locker接口的读写锁

RWMutex为了保证数据的安全,它规定了当有人还在读取数据(即读锁占用)时,不允计有人更新这个数据(即写锁会阻塞);为了保证程序的效率,多个人(线程)读取数据(拥有读锁)时,互不影响不会造成阻塞,它不会像 Mutex 那样只允许有一个人(线程)读取同一个数据。

下面我们使用代码构造一个读多写少的场景,然后分别使用互斥锁和读写锁查看它们的性能差异。

var (
    x       int64
    wg      sync.WaitGroup
    mutex   sync.Mutex
    rwMutex sync.RWMutex
)

// writeWithLock 使用互斥锁的写操作
func writeWithLock() {
    mutex.Lock() // 加互斥锁
    x = x + 1
    time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
    mutex.Unlock()                    // 解互斥锁
    wg.Done()
}

// readWithLock 使用互斥锁的读操作
func readWithLock() {
    mutex.Lock()                 // 加互斥锁
    time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
    mutex.Unlock()               // 释放互斥锁
    wg.Done()
}

// writeWithLock 使用读写互斥锁的写操作
func writeWithRWLock() {
    rwMutex.Lock() // 加写锁
    x = x + 1
    time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
    rwMutex.Unlock()                  // 释放写锁
    wg.Done()
}

// readWithRWLock 使用读写互斥锁的读操作
func readWithRWLock() {
    rwMutex.RLock()              // 加读锁
    time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
    rwMutex.RUnlock()            // 释放读锁
    wg.Done()
}

func do(wf, rf func(), wc, rc int) {
    start := time.Now()
    // wc个并发写操作
    for i := 0; i < wc; i++ {
        wg.Add(1)
        go wf()
    }

    //  rc个并发读操作
    for i := 0; i < rc; i++ {
        wg.Add(1)
        go rf()
    }

    wg.Wait()
    cost := time.Since(start)
    fmt.Printf("x:%v cost:%v\n", x, cost)

}

我们假设每一次读操作都会耗时1ms,而每一次写操作会耗时10ms,我们分别测试使用互斥锁和读写互斥锁执行10次并发写和1000次并发读的耗时数据。

// 使用互斥锁,10并发写,1000并发读
do(writeWithLock, readWithLock, 10, 1000) // x:10 cost:1.466500951s

// 使用读写互斥锁,10并发写,1000并发读
do(writeWithRWLock, readWithRWLock, 10, 1000) // x:10 cost:117.207592ms

从最终的执行结果可以看出,使用读写互斥锁在读多写少的场景下能够极大地提高程序的性能。不过需要注意的是如果一个程序中的读操作和写操作数量级差别不大,那么读写互斥锁的优势就发挥不出来。



  • 标签云

  • 支付宝扫码支持一下

  • 微信扫码支持一下



基于Nginx+Supervisord+uWSGI+Django1.11.1+Python3.6.5构建

京ICP备20007446号-1 & 豫公网安备 41100202000460号

网站地图 & RSS | Feed