sync包接口

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

type Locker interface {
        Lock()
        Unlock()
}

只有两个方法,Lock() 和 Unlock()。
另外该包下的对象,在使用过之后,千万不要复制。
有许多同学不理解锁的概念,下面会一一介绍到:

为什么需要锁?

在并发的情况下,多个线程或协程同时去修改一个变量,可能会出现如下情况:

package main

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

func main() {
    var a = 0

    // 启动 1000 个协程,需要足够大
    // var lock sync.Mutex
    for i := 0; i < 1000; 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)
}

观察打印结果,是否出现 a 的值是相同的情况(未出现则重试或调大协程数),答案:是的。

显然这不是我们想要的结果。出现这种情况的原因是,协程依次执行:从寄存器读取 a 的值 -> 然后做加法运算 -> 最后写会寄存器。试想,此时一个协程取出 a 的值 3,正在做加法运算(还未写回寄存器)。同时另一个协程此时去取,取出了同样的 a 的值 3。最终导致的结果是,两个协程产出的结果相同,a 相当于只增加了 1。

所以,锁的概念就是,我正在处理 a(锁定),你们谁都别和我抢,等我处理完了(解锁),你们再处理。这样就实现了,同时处理 a 的协程只有一个,就实现了同步。

把上面代码里的注释取消掉再试下。

什么是互斥锁 Mutex?

什么是互斥锁?它是锁的一种具体实现,有两个方法:

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

在首次使用后不要复制该互斥锁。对一个未锁定的互斥锁解锁将会产生运行时错误。
一个互斥锁只能同时被一个 goroutine 锁定,其它 goroutine 将阻塞直到互斥锁被解锁(重新争抢对互斥锁的锁定)。如:

package main

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

func main() {
    ch := make(chan struct{}, 2)

    var l sync.Mutex
    go func() {
        l.Lock()
        defer l.Unlock()
        fmt.Println("goroutine1: 我会锁定大概 2s")
        time.Sleep(time.Second * 2)
        fmt.Println("goroutine1: 我解锁了,你们去抢吧")
        ch <- struct{}{}
    }()

    go func() {
        fmt.Println("groutine2: 等待解锁")
        l.Lock()
        defer l.Unlock()
        fmt.Println("goroutine2: 哈哈,我锁定了")
        ch <- struct{}{}
    }()

    // 等待 goroutine 执行结束
    for i := 0; i < 2; i++ {
        <-ch
    }
}

//输出
    goroutine2:等待解锁
    goroutine1:我会锁定大概2s
    goroutine1:我解锁了,你们去抢吧
    goroutine2:哈哈,我锁定了

注意,平时所说的锁定,其实就是去锁定互斥锁,而不是说去锁定一段代码。也就是说,当代码执行到有锁的地方时,它获取不到互斥锁的锁定,会阻塞在那里,从而达到控制同步的目的。

什么是读写锁 RWMutex?

那么什么是读写锁呢?它是针对读写操作的互斥锁,读写锁与互斥锁最大的不同就是可以分别对 读、写 进行锁定。一般用在大量读操作、少量写操作的情况:

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

func (rw *RWMutex) RLock()
func (rw *RWMutex) RUnlock()

由于这里需要区分读写锁定,我们这样定义:

  • 读锁定(RLock),对读操作进行锁定

  • 读解锁(RUnlock),对读锁定进行解锁

  • 写锁定(Lock),对写操作进行锁定

  • 写解锁(Unlock),对写锁定进行解锁

在首次使用之后,不要复制该读写锁。不要混用锁定和解锁,如:Lock 和 RUnlock、RLock 和 Unlock。因为对未读锁定的读写锁进行读解锁或对未写锁定的读写锁进行写解锁将会引起运行时错误。

如何理解读写锁呢?

  • 同时只能有一个 goroutine 能够获得写锁定。

  • 同时可以有任意多个 gorouinte 获得读锁定。

  • 同时只能存在写锁定或读锁定(读和写互斥)。

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

使用例子:

package main

import (
    "fmt"
    "math/rand"
    "sync"
)

var count int
var rw sync.RWMutex

func main() {
    ch := make(chan struct{}, 10)
    for i := 0; i < 5; i++ {
        go read(i, ch)
    }
    for i := 0; i < 5; i++ {
        go write(i, ch)
    }

    for i := 0; i < 10; i++ {
        <-ch
    }
}

func read(n int, ch chan struct{}) {
    rw.RLock()
    fmt.Printf("goroutine %d 进入读操作...\n", n)
    v := count
    fmt.Printf("goroutine %d 读取结束,值为:%d\n", n, v)
    rw.RUnlock()
    ch <- struct{}{}
}

func write(n int, ch chan struct{}) {
    rw.Lock()
    fmt.Printf("goroutine %d 进入写操作...\n", n)
    v := rand.Intn(1000)
    count = v
    fmt.Printf("goroutine %d 写入结束,新值为:%d\n", n, v)
    rw.Unlock()
    ch <- struct{}{}
}

//输出
    goroutine 0 进入读操作...
    goroutine 1 进入读操作...
    goroutine 0 读取结束,值为:0
    goroutine 1 读取结束,值为:0
    goroutine 3 进入读操作...
    goroutine 3 读取结束,值为:0
    goroutine 4 进入写操作...
    goroutine 4 写入结束,新值为:81
    goroutine 2 进入读操作...
    goroutine 4 进入读操作...
    goroutine 4 读取结束,值为:81
    goroutine 2 读取结束,值为:81
    goroutine 1 进入写操作...
    goroutine 1 写入结束,新值为:887
    goroutine 2 进入写操作...
    goroutine 2 写入结束,新值为:847
    goroutine 0 进入写操作...
    goroutine 0 写入结束,新值为:59
    goroutine 3 进入写操作...
    goroutine 3 写入结束,新值为:81

参考文章:



登陆发表评论