首页>>后端>>Golang->【Go并发】— sync包并发同步原语(1)

【Go并发】— sync包并发同步原语(1)

时间:2023-12-02 本站 点击:0

? 放在前面说的话

大家好,我是北 ??

本科在读,此为日常捣鼓.

如有不对,请多指教,也欢迎大家来跟我讨论鸭 ???

今天是我们「Go并发」系列的第三篇:「sync包并发同步原语(1)」;

Let’s get it!

sync包

再并发编程中同步原语也就是我们日常说的锁

保证多线程或多goroutine在访问同一内存时,不出现混乱问题

Go语言提供的sync包提供了常见的并发编程同步原语

sync.Mutex

sync.RWMutex

sync.WaitGroup

sync.Map

sync.Pool

sync.Once

sync.Cond

一、sync.Mutex

在Go并发任务中,容易出现多个goroutine同时操作一个资源,这就会产生竞态问题。这时互斥锁就可以体现出它真正的作用了

1.sync.Mutex概念

Mutex 也称为互斥锁,互斥锁就是互相排斥的锁,它可以用作保护临界区的共享资源,保证同一时刻只有一个 goroutine 操作临界区中的共享资源。

控制共享资源访问方法

保证同一时间有且仅有一个goroutine操作资源(临界区),其他goroutine只能等待锁,直到前面的互斥锁释放,下一个goroutine才能获取锁去操作资源,往后同理

多线程,随机唤醒一个

2. sync.Mutex有以下方法

方法 功能 Lock() 写锁定 Unlock() 写解锁

Mutex 的 Lock 方法和 Unlock 方法要成对使用,不要忘记将锁定的互斥锁解锁,一般做法是使用 defer。

3.sync.Mutex 栗子

不加锁栗子

func main() {

    var count = 0

    var wg sync.WaitGroup

    // 开启十个协程

    for i := 0; i < 10; i++ {

        wg.Add(1)

        go func() {

            defer wg.Done()

            // 一万叠加

            for j := 0; j < 10000; j++ {

                count++

            }

        }()

    }

    wg.Wait()

    fmt.Println(count)

}

打印:73662正确打印:100000原因:多个goroutine在同一片资源出现竟态问题,叠加错误(有时候可能能够正常执行,打印正确,但并不代表后面不会出现错误)### 加锁优化栗子```gofunc main() {    var count = 0    var wg sync.WaitGroup    var m sync.Mutex    // 开启十个协程    for i := 0; i < 10; i++ {        wg.Add(1)        go func() {            defer wg.Done()            // 一万叠加            for j := 0; j < 10000; j++ {                m.Lock()                count++                m.Unlock()            }        }()    }    wg.Wait()    fmt.Println(count)}

打印:100000

4. sync.Mutex和sync.RWMutex比较

RWMutex是基于Mutex的,在Mutex的基础之上增加了读、写的信号量,并使用了类似引用计数的读锁数量,RWMutex 将对临界区的共享资源的读写操作做了区分,RWMutex 可以针对读写操作做不同级别的锁保护。

Mutex在大并发环境下,容易造成锁等待,对性能的影响较大。若某个读/写操作协程加了锁,其他协程就没必要处于等待状态了,也应该可以并发地访问共享变量,这时候,应使用RWMutex,让读/写操作并行,提高性能

二、sync.RWMutex

适用于并发读读,不能进行并发读写

1. sync.RWMutex概念

读锁与读锁兼容,读锁与写锁互斥,写锁与写锁互斥,只有在锁释放后才可以继续申请互斥的锁

可以同时申请多个读锁

有读锁时申请写锁将阻塞,有写锁时申请读锁将阻塞

只要有写锁,后续申请读锁和写锁都将阻塞

2.sync.RWMutex有以下方法

RWMutex 也称为读写互斥锁,读写互斥锁就是读取/写入互相排斥的锁。它可以由任意数量的读取操作的 goroutine 或单个写入操作的 goroutine 持有。 |方法|功能| | --- | --- | |Lock()|申请写锁| |Unlock()|申请释放写锁| |RLock()|申请读锁| |RUnlock()|申请释放读锁| |RLocker()|返回一个实现了Lock()和Unlock()方法的Locker接口|

3.sync.RWMutex栗子

func main() {    var rm sync.RWMutex    for i := 0; i < 3; i++ {        go read(&rm, i)    }    time.Sleep(time.Second * 2)}func read(rm *sync.RWMutex, i int) {    fmt.Println(i, "reader start")    rm.RLock()    fmt.Println(i, "reading")    time.Sleep(time.Second * 1)    rm.RUnlock()    fmt.Println(i, "reader over")}

打印:

打印结果看得出来,2开始,还没有读完,1和0就相继跟上开始读了

三、sync.WaitGroup

1. time.sleep()和sync.WaitGroup比较

理论上waitsleep是完全没有可比性的,一个用于线程间的通信,另一个用于线程阻塞一段时间。唯一相同的就是,sleep方法和wait方法都是用来使线程进入休眠状态的,对于中断信号,都可以进行响应和中断 不同:

语法使用:wait方法必须配合synchronized一起使用,sleep可以单独使用

唤醒方式:sleep需要传递一个超时时间,因此,sleep具有主动唤醒功能,而不需要传递任何参数的wait只能被动唤醒

释放锁资源:wait方法主动释放锁,sleep不释放锁

线程进入状态:sleep有时限等待状态,wait无时限等待状态

2.sync.WaitGroup有以下方法

在并发操作中,生硬使用time.Sleep并不合适,反观前面的比较,sync.WaitGroup更适用于并发任务的同步实现

方法名 功能 Add(num int) 计数器+num Done() 计数器-1 Wait() 阻塞main函数直到计数器为0

sync.WaitGroup内部维护着一个计数器,可增可减。当我们要启动Z个并发任务时,计时器总数要增加到Z,可以通过for循环Add()单个增加,也可以一下子增加到Z;每当一个任务结束时,Done()就要在计数器里-1,直到计数器为0时,调用Wait()表示等待并发任务执行完成。

3. sync.WaitGroup栗子

func main() {    wg := sync.WaitGroup{}    n := 10    wg.Add(n) // 计数器累加至n    for i := 0; i <= n; i++ {        go f(i, &wg)    }    wg.Wait() // 等待计数器值为0,告知main函数的主协程,其他协程执行完毕}func f(i int, wg *sync.WaitGroup) {    fmt.Println(i)    wg.Done() // 完成该协程后,计数器-1}

sync.WaitGroup一定要通过指针传值,不然进程会进入死锁状态

? 放在后面的话

本文我们介绍了 Go 语言中的基本同步原语sync.Mutex、sync.RWMutex、sync.WaitGroup的概念和简单应用,并分别对sync.Mutex和sync.RWMutex,time.sleep和sync.WaitGroup进行了比较。读写互斥锁可以对临界区的共享资源做更加细粒度的访问控制,不限制对临界区的共享资源的并发读,所以在读多写少的场景,我们可以使用读写互斥锁替代互斥锁,提升应用程序的性能。在并发中,我们并不知道完成这一应用程序,我们需要多长时间,time.sleep时有时限的且功能等在并发中,并不算优雅,故sync.WaitGroup更适用于任务编排,等待多个 goroutine 全部完成。

sync包中还有三个比较常用的锁,我们将会在下一篇细说。

原文:https://juejin.cn/post/7102779550815223816


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/Golang/10056.html