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

1,317 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

🎐 放在前面说的话

大家好,我是北 👧🏻

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

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

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

u=4044355992,2659238210&fm=253&fmt=auto&app=138&f=PNG.webp

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在同一片资源出现竟态问题,叠加错误(有时候可能能够正常执行,打印正确,但并不代表后面不会出现错误)

加锁优化栗子

func 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")

}

打印:

image.png

打印结果看得出来,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包中还有三个比较常用的锁,我们将会在下一篇细说。