• 17-Go并发之锁


    互斥锁

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

    sync.Mutex提供两个方法供我们使用。

    方法名功能
    func (m *Mutex) Lock()获取互斥锁
    func (m *Mutex) Unlock()释放互斥锁

    我们在下面的示例代码中使用互斥锁限制每次只有一个 goroutine 才能修改全局变量x,从而修复上面代码中的问题。

    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    var (
    	x  int64
    	wg sync.WaitGroup //等待组
    	m  sync.Mutex     //互斥锁
    )
    
    // add 对全局变量x执行5000次加1操作
    func add() {
    	for i := 0; i < 5000; i++ {
    		m.Lock() //修改x前枷锁
    		x += 1
    		m.Unlock() //改完解锁
    	}
    	wg.Done()
    }
    
    func main() {
    	for i := 0; i < 2; i++ {
    		wg.Add(1)
    		go add()
    	}
    	wg.Wait()
    	fmt.Println(x)
    }
    
    //运行结果10000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    将上面的代码编译后多次执行,每次都会得到预期的10000.

    使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才能获取锁进入临界区,多个goroutine同时等待一个锁,唤醒的策略是随机的。

    读写互斥锁

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

    sync.RWMutex提供了以下5个方法。

    方法名功能
    func (rw *RWMutex) Lock()获取写锁
    func (rw *RWMutex) Unlock()释放写锁
    func (rw *RWMutex) RLock()获取读锁
    func (rw *RWMutex) RUnlock()释放读锁
    func (rw *RWMutex) RLocker() Locker返回一个实现Locker接口的读写锁

    读写锁分为两种:读锁和写锁。当一个 goroutine 获取到读锁之后,其他的 goroutine 如果是获取读锁会继续获得锁,如果是获取写锁就会等待;而当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待。

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

    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)
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    我们假设每一次读操作都会耗时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
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    sync

    Sync.WaitGroup

    在代码中生硬的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步。 sync.WaitGroup有以下几个方法:

    方法名功能
    func (wg * WaitGroup) Add(delta int)计数器+delta
    (wg *WaitGroup) Done()计数器-1
    (wg *WaitGroup) Wait()阻塞直到计数器变为0

    sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了 N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用 Done 方法将计数器减1。通过调用 Wait 来等待并发任务执行完,当计数器值为 0 时,表示所有并发任务已经完成。

    我们利用sync.WaitGroup将上面的代码优化一下:

    var wg sync.WaitGroup
    
    func hello() {
    	defer wg.Done()
    	fmt.Println("Hello Goroutine!")
    }
    func main() {
    	wg.Add(1)
    	go hello() // 启动另外一个goroutine去执行hello函数
    	fmt.Println("main goroutine done!")
    	wg.Wait()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    需要注意sync.WaitGroup是一个结构体,进行参数传递的时候要传递指针。

    sync.Once

    应用的两个条件:

    1. 存在多goroutine并发操作 2. 某个操作只想被执行一次

    确保一个函数在并发的场景下只执行一次。

    只有一个方法:Do(func())

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nb5k3n2j-1656927275583)(day08课上笔记.assets/image-20220306154958722.png)]

    sync.Map

    Go内置的 map 不是并发安全的

    应用场景:

    当一个map变量或者结构体里面的一个map类型的字段 可能会被多个goroutine访问的时候

    方法1:自己加锁

    方法2:sync.Map

    concurrent map writes
    
    • 1
  • 相关阅读:
    2.4 选择结构语句
    Android模拟器中替换库和img
    阿里云2核2G服务器e系列租用优惠价格182元性能测评
    go版本升级
    Mysql 字段值是null或者空串 设置默认值 (case when then)
    Oracle死锁问题: enq: TX - row lock contention
    Pandas中的数据转换[细节]
    《优化接口设计的思路》系列:第五篇—接口发生异常如何统一处理
    杰理之可能出现有些芯片音乐播放速度快【篇】
    springboot+vue+java药房药店在线销售管理系统
  • 原文地址:https://blog.csdn.net/weixin_38753143/article/details/125605110