[Go] 互斥锁和读写互斥锁

690 阅读2分钟

先来看这样一段代码,所存在的问题:

var wg sync.WaitGroup
var x int64

func main() {
	wg.Add(2)
	go f()
	go f()
	wg.Wait()
	fmt.Println(x) // 输出:12135
}

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

这里为什么输出是 12135(不同的机器结果不一样),而不是20000。

因为 x 的赋值,总共分为三个步骤:取出x的值、计算x的结果、给x赋值。那么由于对于f函数的调用采用了goroutine协程,所以存在资源竞争的问题,所以有赋值和计算过程中存在脏数据。对于此类的问题,可以采用互斥锁解决:

互斥锁

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

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

func main() {
	wg.Add(2)
	go f()
	go f()
	wg.Wait()
	fmt.Println(x) // 输出:20000
}

func f()  {
	for i:=0;i<10000;i++ {
		lock.Lock() // 加互斥锁
		x = x+1
		lock.Unlock() // 解锁
	}
	wg.Done() 
}

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

读写互斥锁

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

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

var (
	x1 int64
	wg1 sync.WaitGroup
	lock1 sync.Mutex
	rwlock sync.RWMutex
)

func main() {
	startTime := time.Now()
	for i:=0;i<100;i++ {
		wg1.Add(1)
		write()
	}
	for i:=0;i<10000;i++ {
		wg1.Add(1)
		read()
	}
	wg1.Wait()
	fmt.Println(time.Now().Sub(startTime))

	// 互斥锁用时: 973.9304ms
	// 读写互斥锁用时: 718.094ms
}

func read()  {
	defer wg1.Done()
	//lock1.Lock() // 互斥锁
	rwlock.RLock() // 读写互斥锁
	fmt.Println(x1)
	//lock1.Unlock() // 互斥锁
	rwlock.RUnlock() // 读写互斥锁
}

func write()  {
	defer wg1.Done()
	lock1.Lock()
	x1 = x1+1
	lock1.Unlock()
}