• 首页
  • go
  • go语言 sync 包简介及使用

go语言 sync 包简介及使用


文章目录



sync 包的简介

Go语言的sync包提供了常见的并发编程控制锁;
在并发编程中锁的主要作用是保证多个线程或者 goroutine在访问同一片内存时不会出现混乱;

golang 中使用 go 语句来开启一个新的协程。 goroutine 是非常轻量的,除了给它分配栈空间,它所占用的内存空间是微乎其微的;
但当多个 goroutine 同时进行处理的时候,就会遇到比如同时抢占一个资源,某个 goroutine 等待另一个 goroutine 处理完某一个步骤之后才能继续的需求。
在 golang 的官方文档上,作者明确指出,golang 并不希望依靠共享内存的方式进行进程的协同操作。
而是希望通过管道 channel 的方式进行;

但是一些特殊情况下,我们依然需要用到锁,所以 sync 包提供了我们需要的功能.

整个包都围绕这 Locker 进行,这是一个 interface:

type Locker interface {
        Lock()
        Unlock()
}

接口中定义了2个方法,Lock() 和 Unlock()


为什么需要锁

在并发的情况下,多个线程或协程同时去修改一个变量 , 出现资源抢占 ,数据不一致的问题:


package main

import (
    "fmt"
    "time"
)

func main() {

    var a = 0

    // 启动足够多个协程
    for i := 0; i < 1000000; i++ {
        go func(idx int) {
            a += 1
            fmt.Printf("goroutine %d, a=%d\n", idx, a)
        }(i)
    }

    // 主程序等待1s 确保所有协程执行完
    time.Sleep(time.Second)

}


上面的程序中一次启动了足够多的协程,以达到抢占资源目的,你在测试时可以把协程数量变小来看看会不会出现资源抢占.
我们来看下结果:

结果中俩个不同的协程输出相同的结果

goroutine 19997, a=20088
goroutine 20109, a=20088

这明显不是我们想要的结果, 协程依次从寄存器读取 a 的值 -> 然后做加法运算 -> 最后写会寄存器;
此时一个协程 goroutine 19997 取出 a 的值 20088,正在做加法运算(还未写回寄存器);
同时另一个协程 goroutine 20109 也要去取a的值,取出了同样的 a 的值 20088 (因为上一个协程运行结果还没有写回寄存器);
最终导致的结果是两个协程产出的结果相同.


sync.Mutex

Mutex 互斥锁实现了2个方法

func (m *Mutex) Lock()
func (m *Mutex) Unlock()

对一个未锁定的互斥锁解锁将会产生运行时错误。
一个互斥锁只能同时被一个 goroutine 锁定,其它 goroutine 将阻塞直到互斥锁被解锁(重新争抢对互斥锁的锁定)
我们重新修改下上面的测试代码:

package main

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

func main() {

    var a = 0

    // 启动足够多个协程
    var lock sync.Mutex
    for i := 0; i < 1000000; i++ {
        go func(idx int) {
            lock.Lock()
            defer lock.Unlock()
            a += 1
            fmt.Printf("goroutine %d, a=%d\n", idx, a)
        }(i)
    }

    // 主程序等待1s 确保所有协程执行完
    time.Sleep(time.Second)

}

上面代码我们引入了 sync 包,
代码里定义了 Mutex 类型变量 lock,
协程里面我们给互斥锁加锁,
确保协程执行完成后, defer lock.Unlock() 执行解锁操作.

查看结果:

结果不仅没有发现抢占资源导致的输出重复,而且输出结果顺序递增.


Mutex类型互斥锁原理

平时所说的锁定,其实就是去锁定互斥锁,而不是说去锁定一段代码;
这和其他编程语言有所区别,例如java 中我们会用同步锁锁定一段代码,确保多线程并发时只有一个线程可以控制运行此代码段,知道释放同步锁;
而go语言是在 goroutine 中锁定互斥锁 ,其他 goroutine 执行到有锁的地方时,它获取不到互斥锁的锁定,会阻塞在那里等待,从而达到控制同步的目的.


sync.RWMutex

RWMutex 读写锁 基于 Mutex 实现
读写锁的特点 : 它是针对读写操作的互斥锁,读写锁与互斥锁最大的不同就是可以分别对 读、写 进行锁定。
一般用在大量读操作、少量写操作的情况.

  • 同时只能有一个 goroutine 能够获得写锁定
  • 同时可以有任意多个 gorouinte 获得读锁定
  • 同时只能存在写锁定或读锁定(读和写互斥)

当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;
当有一个 goroutine 获得读锁定,其它读锁定任然可以继续;
当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定;
所以说这里的读锁定(RLock)目的其实是告诉写锁定:有很多人正在读取数据,你需要排队等待;

实现的方法:

func (rw *RWMutex) Lock()
func (rw *RWMutex) Unlock()

func (rw *RWMutex) RLock()
func (rw *RWMutex) RUnlock()
  • 读锁定(RLock),对读操作进行锁定
  • 读解锁(RUnlock),对读锁定进行解锁
  • 写锁定(Lock),对写操作进行锁定
  • 写解锁(Unlock),对写锁定进行解锁
    上面的锁成对使用,不能互相拆分混用,会发生运行时错误

package main

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

var m *sync.RWMutex
var val = 0

func main() {
    m = new(sync.RWMutex)
    for i := 0; i < 5; i++ {
        go read(1)
    }
    for j := 0; j < 5; j++ {
        go write(2)
    }
    for m := 0; m < 5; m++ {
        go read(3)
    }

    time.Sleep(18 * time.Second)
}

func read(i int) {
    m.RLock()
    println("读: ", i, val)
    time.Sleep(3 * time.Second)
    println("读结束")
    m.RUnlock()
}

func write(i int) {
    m.Lock()
    val = 10
    println("写: ", i, val)
    time.Sleep(3 * time.Second)
    println("写结束")
    m.Unlock()
}

上面 read 方法应用了读锁RLock;
write 方法应用了写锁 Lock;
为了验证协程抢夺资源, 读写方法中我们让程序休眠一定的时间;

main 函数中的 time.Sleep 是为防止主程序提前退出,goroutine 协程函数还未执行完.

查看运行结果:

# zhangzhi @ ZhangZhi-MacBook-Pro in ~/code/go/program [19:08:18] 
$ go run rwlock.go
读:  1 0
读结束
写:  2 10
写结束
读:  3 10
读:  3 10
读:  1 10
读:  1 10
读:  3 10
读:  1 10
读:  1 10
读:  3 10
读:  3 10
读结束
读结束
读结束
读结束
读结束
读结束
读结束
读结束
读结束
写:  2 10
写结束
写:  2 10
写结束
写:  2 10

注意:每次执行的结果都有差异,当你复制上面代码运行,结果和上面有所不同
但是:结果展示出来的规律却是一致的:

规律一:[同时可以有任意多个 gorouinte 获得读锁定]
RWMutex 读锁可以并发多个执行,从上面read 程序和程序执行输出的内容来看
说明在加上读取锁时,其他 goroutine 依然可以并发多个 读 访问.

规律二: [同时只能有一个 goroutine 能够获得写锁定]
RWMutex 写获得锁定时,不论程序休眠多长时间,一定会输出 写结束,其他 goroutine 才能获得锁资源.

规律三: [同时只能存在写锁定或读锁定(读和写互斥)]
读虽然可以同时多个 goroutine 来锁定,但是写锁定之前其他多个读锁定必须全部释放锁.
写锁定获得锁时,其他 读 或者 写 都无法再获得锁,直到此 goroutine 写结束,释放锁后,其他 goroutine 才会争夺.
所以 读和写 的俩种锁是互斥的.

RWMutex 读写锁使用于读多写少的业务逻辑


sync.WaitGroup

它的使用场景是在一个goroutine等待一组goroutine执行完成.
WaitGroup拥有一个内部计数器;
当计数器等于0时,则Wait()方法会立即返回;
否则它将阻塞执行Wait()方法的goroutine直到计数器等于0时为止;

增加计数器,使用Add(int)方法。
减少计数器,我们可以使用Done()(将计数器减1),
也可以传递负数给Add方法把计数器减少指定大小,Done()方法底层就是通过Add(-1)实现的.

/*
 * @Author: your name
 * @Date: 2021-01-13 19:45:43
 * @LastEditTime: 2021-01-13 19:45:44
 * @LastEditors: Please set LastEditors
 * @Description: In User Settings Edit
 * @FilePath: /program/waitgroup.go
 */
package main

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

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        // 计数加 1
        wg.Add(1)
        go func(i int) {
            // 计数减 1
            defer wg.Done()
            time.Sleep(time.Second * time.Duration(i))
            fmt.Printf("goroutine%d 结束\n", i)
        }(i)
    }

    // 等待执行结束
    wg.Wait()
    fmt.Println("所有 goroutine 执行结束")
}

执行结果:

# zhangzhi @ ZhangZhi-MacBook-Pro in ~/code/go/program [19:46:01] 
$ go run waitgroup.go 
goroutine0 结束
goroutine1 结束
goroutine2 结束
goroutine3 结束
goroutine4 结束
所有 goroutine 执行结束

出自:go语言 sync 包简介及使用

回到顶部