Golang语言goroutine协程并发安全及锁机制

                                              作者:尹正杰

版权声明:原创作品,谢绝转载!否则将追究法律责任。

一.多协程操作同一数据问题引出

package main

import (
	"fmt"
	"sync"
)

var (
	count int
	wg    sync.WaitGroup
)

func add() {
	defer wg.Done()

	for i := 1; i <= 1000000; i++ {
		count++
	}
}

func sub() {
	defer wg.Done()

	for i := 1; i <= 1000000; i++ {
		count--
	}
}

func main() {
	// 开启2个协程等待
	wg.Add(2)

	go add()
	go sub()

	wg.Wait()

	// 在理论上,这个count结果应该是0,无论协程怎么交替执行,最终咱们想象的结果就是0,但事实上并不是!!!
	// 那为什么结果不为0呢?可以参考上图的流程,最终值不为0的执行思路。
	fmt.Printf("count = %d\n", count)
}

二.互斥锁Mutex

1 互斥锁概述

方法名 功能
func (m *Mutex) Lock() 获取互斥锁
func (m *Mutex) Unlock() 释放互斥锁
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同一时间只有一个goroutine可以访问共享资源。

Go语言中使用sync包中提供的Mutex类型来实现互斥锁。

使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁。

当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。

2使用互斥锁Mutex同步协程

package main

import (
	"fmt"
	"sync"
)

var (
	count int
	wg    sync.WaitGroup
	/*
	互斥锁:
		"sync.Mutex"为互斥锁,"Lock()"进行加锁,"Unlock()"进行解锁。
		
		使用"Lock()"加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁。
		
		互斥锁适用于读写不确定场景,即读写次数没有明显的区别,性能相对来说比较低(因为同一个时刻仅有一个协程可以操作)。
	*/
	lock sync.Mutex
)

func add() {
	defer wg.Done()

	for i := 1; i <= 1000000; i++ {
		// 加锁,确保一个协程在执行逻辑的时候另外的协程不被执行。
		lock.Lock()
		count++
		// 解锁
		lock.Unlock()
	}
}

func sub() {
	defer wg.Done()

	for i := 1; i <= 1000000; i++ {
		// 加锁
		lock.Lock()
		count--
		// 解锁
		lock.Unlock()
	}
}

func main() {
	// 开启2个协程等待
	wg.Add(2)

	go add()
	go sub()

	wg.Wait()

	// 使用互斥锁同步协程,从而解决多协程在对同一个资源处理时数据同步的问题。
	fmt.Printf("count = %d\n", count)
}

三.读写互斥锁RWMutex

1 读写互斥锁概述

方法名 功能
func (rw *RWMutex) Lock() 获取写锁
func (rw *RWMutex) Unlock() 释放写锁
func (rw *RWMutex) RLock() 获取读锁
func (rw *RWMutex) RUnlock() 释放读锁
func (rw *RWMutex) RLocker() Locker 返回一个实现Locker接口的读写锁
互斥锁是完全互斥的,但是实际上有很多场景是读多写少的,当我们并发的去读取一个资源而不涉及资源修改的时候是没有必要加互斥锁的。

这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。

sync.RWMutex提供了如上表所示的5个方法。读写锁分为两种:读锁和写锁。

当一个goroutine获取到读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待。

而当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。

2 读写锁RWMutex引入

package main

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

var (
	/*
		读写锁:
			RWMutex是一个读写锁,其经常用于读次数远远多于写次数的场景。

			读写锁的好处是多个协程同时在读的时候,数据之间不产生影响,锁不产生影响。

			但多个协程出现了同时读和写的情况下才会产生影响,锁就会产生影响。
	*/
	lock sync.RWMutex

	wg sync.WaitGroup
)

func read(id int) {
	defer wg.Done()

	// 加读锁,如果只是读数据,那么这个锁不产生影响,但是读写同时发生的时候,就会有影响
	lock.RLock()

	fmt.Printf("开始读取数据...ID = %d\n", id)
	time.Sleep(time.Second * 5)
	fmt.Printf("读取数据完成...ID = %d\n", id)

	// 解读锁
	lock.RUnlock()

}

func write() {
	defer wg.Done()

	lock.Lock()
	fmt.Printf("开始修改数据...\n")
	time.Sleep(time.Second * 3)
	fmt.Printf("修改数据完成...\n")
	lock.Unlock()

}

func main() {
	wg.Add(11)

	// 启动协程: 模拟"读多写少"场景,10个线程读(无视锁),总耗时仅需5秒。
	for i := 1; i <= 10; i++ {
		go read(i)
	}

	go write()

	wg.Wait()

	fmt.Println("main程序运行完成,程序已退出!")
}

posted @ 2024-08-04 00:21  尹正杰  阅读(133)  评论(0编辑  收藏  举报