在日常生活中,锁是为了保护一些东西,比如门锁、密码箱锁,可以理解对资源的保护。
在编程世界里,锁也是为了保护资源,比如文件锁 : 同一时间只也许一个用户修改文件。
本文就梳理了一下 Golang 中 sync 包中的锁机制。希望对您有帮助 ~
sync.Mutex(互斥锁)
这是一个标准的互斥锁,平时用的也比较多,用法也非常简单,lock用于加锁,unlock用于解锁,配合defer使用,完美。
sync.RWMutex(读写锁)
读写锁是互斥锁的升级版,它最大的优点就是支持多读,但是读和写、以及写与写之间还是互斥的,所以比较适合读多写少的场景。
| 1
2
3
4
5
 | func (rw *RWMutex) Lock()            // 申请写锁
func (rw *RWMutex) Unlock()	         // 释放写锁
func (rw *RWMutex) RLock()           // 申请读锁
func (rw *RWMutex) RUnlock()         // 释放读锁
func (rw *RWMutex) RLocker() Locker  // 返回一个实现了Lock()和Unlock()方法的Locker接口
 | 
 
注 : 读性能上面比 Mutex 要好。
sync.Map(并发安全的 Map )
标准库里面实现的 sync.Map,是一个自带锁的map,使用起来方便省心。
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
 | package main
var m sync.Map
func main() {
    m.Store("1", 1)
    m.Store("2", 1)
    m.Store("3", 1)
    m.Store(4, "5") // 注意类型
    load, ok := m.Load("1")
    if ok {
        fmt.Printf("%v\n", load)
    }
    load, ok = m.Load(4)
    if ok {
        fmt.Printf("%v\n", load)
    }
}
 | 
 
sync.Once
sync.Once的功能就是保证只执行一次,也算是一种锁,通常可以用于只能执行一次的初始化操作,比如说单例模式里面的懒汉模式可以用到。
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 | package main
import "sync"
var once sync.Once
func main() {
    doOnce()
    doOnce()
    doOnce()
}
func doOnce() {
    once.Do(func() {
        println("one")
    })
}
 | 
 
sync.Cond(条件锁)
这个一般称之为条件锁,就是当满足某些条件下才起作用的锁,啥个意思呢?举个例子,当我们执行某个操作需要先获取锁,但是这个锁必须是由某个条件触发的,其中包含三种方式:
- 等待通知: wait,阻塞当前线程,直到收到该条件变量发来的通知。
- 单发通知: signal,让该条件变量向至少一个正在等待它的通知的线程发送通知,表示共享数据的状态已经改变。
- 广播通知: broadcast,让条件变量给正在等待它的通知的所有线程都发送通知。
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 | package main
import (
    "sync"
    "time"
)
var cond = sync.NewCond(&sync.Mutex{})
func main() {
    for i := 0; i < 10; i++ {
        go func(i int) {
            cond.L.Lock()
            cond.Wait() // 等待通知,阻塞当前goroutine
            println(i)
            cond.L.Unlock()
        }(i)
    }
    // 确保所有协程启动完毕
    time.Sleep(time.Second * 1)
    cond.Signal()
    cond.Signal()
    cond.Signal()
    // 确保结果有时间输出
    time.Sleep(time.Second * 1)
}
 | 
 
sync.WaitGroup(一般用来控制协程执行顺序)
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 | package main
import "sync"
var wg sync.WaitGroup
func main() {
    for i := 0; i < 10; i++ {
        wg.Add(1) // 计数+1
        go func() {
            println("1")
            wg.Done() // 计数-1,相当于wg.add(-1)
        }()
    }
    wg.Wait() // 阻塞带等待所有协程执行完毕
}
 | 
 
sync.Pool
这是一个池子,但是却是一个不怎么可靠的池子,sync.Pool 初衷是用来保存和复用临时对象,以减少内存分配,降低CG压力。
说它不可靠是指放进 Pool 中的对象,会在说不准什么时候被GC回收掉,所以如果事先 Put 进去 100 个对象,下次 Get 的时候发现 Pool 是空也是有可能的。
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 | package main
import (
    "fmt"
    "sync"
)
type User struct {
    name string
}
var pool = sync.Pool{
    New: func() interface{} {
        return User{
            name: "default name",
        }
    },
}
func main() {
    pool.Put(User{name: "name1"})
    pool.Put(User{name: "name2"})
    fmt.Printf("%v\n", pool.Get()) // {name1}
    fmt.Printf("%v\n", pool.Get()) // {name2}
    fmt.Printf("%v\n", pool.Get()) // {default name} 池子已空,会返回New的结果
}
 | 
 
从上述输出结果可以看到,Pool就像是一个池子,我们放进去什么东西,但不一定可以取出来(如果中间有GC的话就会被清空),如果池子空了,就会使用之前定义的New方法返回的结果。
注 : 为什么这个池子会放到sync包里面呢?那是因为它有一个重要的特性就是协程安全的,所以其底层自然也用到锁机制。
参考
https://wangbjun.site/2020/coding/golang/locker.html