互斥锁和读写互斥锁

wxvirus2022年6月28日
大约 9 分钟

Go 中的锁

看下面一段代码,同时竞争全局变量x求和。

package main

import (
	"fmt"
	"sync"
)

var (
	x int64
	wg sync.WaitGroup
)

func add()  {
	for i := 0; i < 5000; i++ {
		x += 1
	}
	wg.Done()
}

func main() {
	wg.Add(2)

	go add()
	go add()

	wg.Wait()
	fmt.Println(x)
}

这样下来,每次的结果多会不一样,可能你前脚把x加到 100,后一个 goroutine 又加到了 300,最终导致结果不是我们想要的 10000

提示

即我们最终得对一个公共的资源不能同时处理数据。不然就会产生数据竞争,并发不安全。像我们上厕所一样,当有一个人在厕所,会把门关上加个锁,这样其余的人就不能进去,等你完事后再把锁打开,别人又可以进去了。

互斥锁

获取到互斥锁的任务,阻塞其他任务来获取锁

意味着同一时间只能有一个任务才能持有互斥锁

使用互斥锁来修改上面的代码:

package main

import (
	"fmt"
	"sync"
)

var (
	x int64
	wg sync.WaitGroup
	lock sync.Mutex
)

func add()  {
	for i := 0; i < 5000; i++ {
		lock.Lock() // 改之前加锁
		x += 1 // 代码到了这里就变成了串行
		lock.Unlock() // 改完解锁
	}
	wg.Done()
}

func main() {
	wg.Add(2)

	go add()
	go add()

	wg.Wait()
	fmt.Println(x)
}

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

package main

import (
	"log"
	"sync"
	"time"
)

// HcMutex 是一个互斥锁
var HcMutex sync.Mutex

func runMutex(id int) {
	log.Printf("[任务id:%d][尝试获取锁]", id)
	HcMutex.Lock()
	log.Printf("[任务id:%d][获取到了锁]", id)
	time.Sleep(20 * time.Second) // 睡眠10秒钟
	HcMutex.Unlock()
	log.Printf("[任务id:%d][干完活了,释放锁]", id)
}

func runHcLock() {
	go runMutex(1)
	go runMutex(2)
	go runMutex(3)
}

func main() {
	runHcLock()

	time.Sleep(6 * time.Minute)

	/*
		2021/09/02 22:15:46 [任务id:3][尝试获取锁]
		2021/09/02 22:15:46 [任务id:3][获取到了锁]
		2021/09/02 22:15:46 [任务id:1][尝试获取锁]
		2021/09/02 22:15:46 [任务id:2][尝试获取锁]
		2021/09/02 22:15:56 [任务id:1][获取到了锁]
		2021/09/02 22:15:56 [任务id:3][干完活了,释放锁]
		2021/09/02 22:16:06 [任务id:1][干完活了,释放锁]
		2021/09/02 22:16:06 [任务id:2][获取到了锁]
		2021/09/02 22:16:16 [任务id:2][干完活了,释放锁]
	*/
}

读写锁

package main

import (
	"log"
	"sync"
	"time"
)

// rwMutex 是一个读写锁
var rwMutex sync.RWMutex

func runReadLock(id int) {
	log.Printf("[读任务id:%d][进入读方法,尝试获取锁]", id)
	rwMutex.RLock() // 读锁
	log.Printf("[读任务id:%d][获取到了读锁][开始干活,睡眠10秒]", id)
	time.Sleep(10 * time.Second) // 睡眠10秒钟
	rwMutex.RUnlock()
	log.Printf("[读任务id:%d][完成读任务,释放读锁]", id)
}

func runWriteLock(id int) {
	log.Printf("[写任务id:%d][进入写方法,尝试获取锁]", id)
	rwMutex.Lock() // 写锁
	log.Printf("[写任务id:%d][获取到了写锁][开始干活,睡眠10秒]", id)
	time.Sleep(10 * time.Second) // 睡眠10秒钟
	rwMutex.Unlock()
	log.Printf("[写任务id:%d][完成写任务,释放写锁]", id)
}

// 全是写任务
func allWriteWorks() {
	for i := 1; i <= 5; i++ {
		go runWriteLock(i)
	}
}

// 全是读任务
func allReadWorks() {
	for i := 1; i <= 5; i++ {
		go runReadLock(i)
	}
}

// 先启动写任务
func writeFirst() {
	go runWriteLock(1)
	time.Sleep(1 * time.Second)
	go runReadLock(1)
	go runReadLock(2)
	go runReadLock(3)
	go runReadLock(4)
	go runReadLock(5)
}

// 先启动写任务
func readFirst() {
	go runReadLock(1)
	go runReadLock(2)
	go runReadLock(3)
	go runReadLock(4)
	go runReadLock(5)
	time.Sleep(1 * time.Second)
	go runWriteLock(1)
}

func main() {
	log.Printf("执行读写锁效果的函数")

	// 1. 同时多个写锁任务 =>  如果并发使用读写锁的写锁时,退化成了互斥锁
	//allWriteWorks()

	/*
		2021/09/02 22:27:28 执行读写锁效果的函数
		2021/09/02 22:27:28 [写任务id:5][进入写方法,尝试获取锁]
		2021/09/02 22:27:28 [写任务id:5][获取到了写锁][开始干活,睡眠10秒]
		2021/09/02 22:27:28 [写任务id:1][进入写方法,尝试获取锁]
		2021/09/02 22:27:28 [写任务id:2][进入写方法,尝试获取锁]
		2021/09/02 22:27:28 [写任务id:3][进入写方法,尝试获取锁]
		2021/09/02 22:27:28 [写任务id:4][进入写方法,尝试获取锁]
		2021/09/02 22:27:38 [写任务id:1][获取到了写锁][开始干活,睡眠10秒]
		2021/09/02 22:27:38 [写任务id:5][完成写任务,释放写锁]
		2021/09/02 22:27:48 [写任务id:1][完成写任务,释放写锁]
		2021/09/02 22:27:48 [写任务id:2][获取到了写锁][开始干活,睡眠10秒]
		2021/09/02 22:27:58 [写任务id:2][完成写任务,释放写锁]
		2021/09/02 22:27:58 [写任务id:3][获取到了写锁][开始干活,睡眠10秒]
		2021/09/02 22:28:08 [写任务id:3][完成写任务,释放写锁]
		2021/09/02 22:28:08 [写任务id:4][获取到了写锁][开始干活,睡眠10秒]
		2021/09/02 22:28:18 [写任务id:4][完成写任务,释放写锁]
	*/

	// 2. 同时多个读锁任务,使用读锁,可以同时加多把锁
	//allReadWorks()

	/*
		2021/09/02 22:29:16 执行读写锁效果的函数
		2021/09/02 22:29:16 [读任务id:5][进入读方法,尝试获取锁]
		2021/09/02 22:29:16 [读任务id:2][进入读方法,尝试获取锁]
		2021/09/02 22:29:16 [读任务id:4][进入读方法,尝试获取锁]
		2021/09/02 22:29:16 [读任务id:2][获取到了读锁][开始干活,睡眠10秒]
		2021/09/02 22:29:16 [读任务id:4][获取到了读锁][开始干活,睡眠10秒]
		2021/09/02 22:29:16 [读任务id:5][获取到了读锁][开始干活,睡眠10秒]
		2021/09/02 22:29:16 [读任务id:1][进入读方法,尝试获取锁]
		2021/09/02 22:29:16 [读任务id:1][获取到了读锁][开始干活,睡眠10秒]
		2021/09/02 22:29:16 [读任务id:3][进入读方法,尝试获取锁]
		2021/09/02 22:29:16 [读任务id:3][获取到了读锁][开始干活,睡眠10秒]
		2021/09/02 22:29:26 [读任务id:3][完成读任务,释放读锁]
		2021/09/02 22:29:26 [读任务id:4][完成读任务,释放读锁]
		2021/09/02 22:29:26 [读任务id:2][完成读任务,释放读锁]
		2021/09/02 22:29:26 [读任务id:5][完成读任务,释放读锁]
		2021/09/02 22:29:26 [读任务id:1][完成读任务,释放读锁]
	*/

	// 3. 先启动写锁,后并发5个读锁任务
	// 当有写锁存在时,读锁是施加不了的。写锁释放完,写锁可以并发的施加多个
	// 写锁阻塞所有读锁
	//writeFirst()

	/*
		2021/09/02 22:31:53 执行读写锁效果的函数
		2021/09/02 22:31:53 [写任务id:1][进入写方法,尝试获取锁]
		2021/09/02 22:31:53 [写任务id:1][获取到了写锁][开始干活,睡眠10秒]
		2021/09/02 22:31:54 [读任务id:2][进入读方法,尝试获取锁]
		2021/09/02 22:31:54 [读任务id:5][进入读方法,尝试获取锁]
		2021/09/02 22:31:54 [读任务id:4][进入读方法,尝试获取锁]
		2021/09/02 22:31:54 [读任务id:1][进入读方法,尝试获取锁]
		2021/09/02 22:31:54 [读任务id:3][进入读方法,尝试获取锁]
		2021/09/02 22:32:03 [读任务id:5][获取到了读锁][开始干活,睡眠10秒]
		2021/09/02 22:32:03 [读任务id:3][获取到了读锁][开始干活,睡眠10秒]
		2021/09/02 22:32:03 [写任务id:1][完成写任务,释放写锁]
		2021/09/02 22:32:03 [读任务id:2][获取到了读锁][开始干活,睡眠10秒]
		2021/09/02 22:32:03 [读任务id:4][获取到了读锁][开始干活,睡眠10秒]
		2021/09/02 22:32:03 [读任务id:1][获取到了读锁][开始干活,睡眠10秒]
		2021/09/02 22:32:13 [读任务id:1][完成读任务,释放读锁]
		2021/09/02 22:32:13 [读任务id:2][完成读任务,释放读锁]
		2021/09/02 22:32:13 [读任务id:4][完成读任务,释放读锁]
		2021/09/02 22:32:13 [读任务id:5][完成读任务,释放读锁]
		2021/09/02 22:32:13 [读任务id:3][完成读任务,释放读锁]
	*/

	// 4. 先启动读锁,先并发5个读锁任务,后启动一个写锁任务
	// 当有读锁时,阻塞写锁
	readFirst()

	/*
		2021/09/02 22:34:19 执行读写锁效果的函数
		2021/09/02 22:34:19 [读任务id:5][进入读方法,尝试获取锁]
		2021/09/02 22:34:19 [读任务id:5][获取到了读锁][开始干活,睡眠10秒]
		2021/09/02 22:34:19 [读任务id:3][进入读方法,尝试获取锁]
		2021/09/02 22:34:19 [读任务id:2][进入读方法,尝试获取锁]
		2021/09/02 22:34:19 [读任务id:2][获取到了读锁][开始干活,睡眠10秒]
		2021/09/02 22:34:19 [读任务id:3][获取到了读锁][开始干活,睡眠10秒]
		2021/09/02 22:34:19 [读任务id:4][进入读方法,尝试获取锁]
		2021/09/02 22:34:19 [读任务id:4][获取到了读锁][开始干活,睡眠10秒]
		2021/09/02 22:34:19 [读任务id:1][进入读方法,尝试获取锁]
		2021/09/02 22:34:19 [读任务id:1][获取到了读锁][开始干活,睡眠10秒]
		2021/09/02 22:34:20 [写任务id:1][进入写方法,尝试获取锁]
		2021/09/02 22:34:29 [读任务id:3][完成读任务,释放读锁]
		2021/09/02 22:34:29 [读任务id:1][完成读任务,释放读锁]
		2021/09/02 22:34:29 [读任务id:4][完成读任务,释放读锁]
		2021/09/02 22:34:29 [读任务id:2][完成读任务,释放读锁]
		2021/09/02 22:34:29 [读任务id:5][完成读任务,释放读锁]
		2021/09/02 22:34:29 [写任务id:1][获取到了写锁][开始干活,睡眠10秒]
		2021/09/02 22:34:39 [写任务id:1][完成写任务,释放写锁]
	*/
	time.Sleep(1 * time.Hour)
}

结论

  1. 同时多个写锁任务 -> 如果并发使用读写锁的写锁时,退化成了互斥锁
  2. 同时多个读锁任务,使用读锁,可以同时加多把锁
  3. 当有写锁存在时,读锁是施加不了的。写锁释放完,写锁可以并发的施加多个
  4. 先启动读锁,先并发 5 个读锁任务,后启动一个写锁任务,当有读锁时,阻塞写锁
Loading...