Golang 并发安全和锁:互斥锁、读写锁

互斥锁

var wg sync.WaitGroup

func main() {
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go test()
	}
	wg.Wait()
}

var count = 0 // 定义count

func test() {
	count++
	fmt.Println(count)
	wg.Done()
}

// 普通编译没有问题,但如果使用 go build -race main.go 进行编译,运行,会发现存在竞争情况 Found 1 data race(s)
// 此时,可以使用互斥锁来解决多个协程同时操作的情况

互斥锁,是传统并发编程中,对共享资源进行访问控制的主要手段

  • Golang 中,互斥锁,使用 sync.Mutex 结构体表示,该结构体只有两个公开的指针方法:
    • Lock(),锁定当前的共享资源
    • Unlock(),进行解锁

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

var wg sync.WaitGroup
var mutex sync.Mutex // mutex,互斥的意思

func main() {
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go test()
	}
	wg.Wait()
}

var count = 0 // 定义count

// 使用互斥锁,锁定当前的共享资源,则一次只能一个协程操作
func test() {
	mutex.Lock() // 加锁
	count++
	fmt.Println(count)
	wg.Done()
	mutex.Unlock() // 解锁
}

读写( 互斥 )锁

互斥锁的本质是,当一个协程访问的时候,其他协程都不能访问。但是,在资源同步、避免竞争的同时,也降低了程序的并发性能,程序由之前的并行执行,变成了串行执行

实际上,真正的互斥,应该在写( 读取和修改、修改和修改 ),而读,是没有互斥操作的必要的

  • 当我们对一个不会变化的数据,进行读操作的时候,是不会存在资源竞争的问题的,因为数据是不变的,不管怎么读取,多少个协程同时读取,都是可以的
  • 问题主要出行在 “修改”,也就是写操作上,修改的数据要同步,这样其他的协程才会感知到

因此,衍生出了另外一种锁,读写( 互斥 )锁,其特点是:

  • 多个读操作,可以并发执行
  • 但是,对于写操作是完全互斥的,即,一个协程进行写操作时,其他协程既不能进行读操作,也不能进行写操作

「 基本语法 」读写锁,由结构体 sync.RWMutex 表示,该结构体的方法集合中,包含两对方法:

  • 对写操作的加锁和解锁  func (*RWMutex)Lock()、func (*RWMutex)Unlock()
  • 对读操作的解锁和解锁  func (*RWMutex)RLock()、func (*RWMutex)RUnlock()
var wg sync.WaitGroup
var mutex sync.RWMutex

func main() {

	// 开启10个协程进行读操作
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go read()
	}

	// 开启10个协程进行写操作
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go write()
	}

	wg.Wait()
}

// 写操作
func write() {
	mutex.Lock() // 加锁
	fmt.Println("写操作")
	time.Sleep(time.Second)
	wg.Done()
	mutex.Unlock() // 解锁
}

// 读操作
func read() {
	mutex.RLock() // 读写锁 -  加锁
	fmt.Println("读操作")
	time.Sleep(time.Second)
	wg.Done()
	mutex.RUnlock() // 读写锁 - 解锁
}