go语言中的互斥锁以及读写互斥锁

3 阅读3分钟

会造成多个协程输出相同的数字?

package main

import (
	"fmt"
	"sync"
	"time"
)

var count = 0
var wg sync.WaitGroup

func test() {
	count++
	fmt.Printf("count的数据为:%d\n", count)
	time.Sleep(time.Microsecond)
	wg.Done()
}

func main() {
	for r := 0; r < 200; r++ {
		wg.Add(1)
		go test()
	}
	wg.Wait()
	fmt.Printf("程序执行完成")
}

会。程序存在数据竞争,多个 goroutine 很可能输出相同数字

解决办法:互斥锁

互斥锁是传统并发编程中对共享资源进行访问控制的主要手段,
它由标准库sync中的Mutex结构体类型表示。
sync.Mutex类型只有两个公开的指针方法,
Lock和Unlock。Lock锁定当前的共享资源,Unlock进行解锁

package main

import (
	"fmt"
	"sync"
	"time"
)

var count = 0
var wg sync.WaitGroup

// 声明一个互斥锁,用于保护 count 变量的访问
var mutex sync.Mutex

func test() {
	// 加锁
	mutex.Lock()
	count++
	fmt.Printf("count的数据为:%d\n", count)
	time.Sleep(time.Microsecond)
	// 解锁
	mutex.Unlock()
	wg.Done()
}

func main() {
	for r := 0; r < 200; r++ {
		wg.Add(1)
		go test()
	}
	wg.Wait()
	fmt.Printf("程序执行完成")
}

读写互斥锁

互斥锁的本质是当一个goroutine访问的时候,其他goroutine都不能访问。
这样在资源同步,避免竞争的同时也降低了程序的并发性能。
程序由原来的并行执行变成了串行执行。
其实,当我们对一个不会变化的数据只做“读”操作的话,是不存在资源竞争的问题的。
因为数据是不变的,不管怎么读取,多少goroutine同时读取,都是可以的。
所以问题不是出在“读”上,主要是修改,也就是“写”。
修改的数据要同步,这样其他 goroutine 才可以感知到。
所以真正的互斥应该是读取和修改、修改和修改之间,
读和读是没有互斥操作的必要的。
因此,衍生出另外一种锁,叫做读写锁。
读写锁可以让多个读操作并发,同时读取,但是对于写操作是完全互斥的。
也就是说,当一个 goroutine 进行写操作的时候,
某他 goroutine 既不能进行读操作,也不能进行写操作。

读写锁的总结

读写锁可以让多个读操作并发,同时读取,但是对于写操作是完全互斥的。
也就是说,当一个goroutine进行写操作的时候,其他goroutine既不能进行读操作,也不能进行写操作。

读写锁的使用

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup
var mu sync.RWMutex

func WriteData() {
	mu.Lock() // 加写锁
	fmt.Printf("写入数据	\n")
	time.Sleep(time.Second * 2)
	mu.Unlock() // 解锁
	wg.Done()
}

func ReadData() {
	// 读写锁可以让多个读操作并发,同时读取数据
	mu.RLock() // 加读锁
	fmt.Printf("读取数据---\n")
	time.Sleep(time.Second * 2)
	mu.RUnlock() // 解读锁
	wg.Done()
}

func main() {
	for r := 0; r < 10; r++ {
		wg.Add(1)
		go WriteData()
	}
	for r := 0; r < 10; r++ {
		wg.Add(1)
		go ReadData()
	}
	wg.Wait()
	fmt.Printf("程序执行完成")
}

写入数据 读取数据---读取数据---读取数据---读取数据---读取数据---读取数据---读取数据---读取数据---读取数据---读取数据---写入数据 写入数据 写入数据 写入数据 写入数据 写入数据 写入数据 写入数据 写入数据 程序执行完成