脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Golang - Golang的锁机制与使用技巧小结

Golang的锁机制与使用技巧小结

2022-10-12 11:52树獭叔叔 Golang

本文主要介绍了Golang的锁机制与使用技巧小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

1. sync.Mutex详解

sync.Mutex是Go中的互斥锁,通过.lock()方法上锁,.unlock()方法解锁。需要注意的是,因为Go函数值传递的特点,sync.Mutex通过函数传递时,会进行一次拷贝,所以传递过去的锁是一把全新的锁,大家在使用时要注意这一点,另外sync.Mutex是非重入锁,这一点要与Java中的锁区分。

?
1
2
3
4
type Mutex {
    state int32
    sema  uint32
}

上面数据结构中的state最低三位分别表示 mutexLocked、mutexWoken 和 mutexStarving,剩下的位置用来表示当前有多少个 Goroutine 等待互斥锁的释放:

?
1
2
3
4
5
6
7
8
32                                               3             2             1             0
 |                                               |             |             |             |
 |                                               |             |             |             |
 v-----------------------------------------------v-------------v-------------v-------------+
 |                                               |             |             |             v
 |                 waitersCount                  |mutexStarving| mutexWoken  | mutexLocked |
 |                                               |             |             |             |
 +-----------------------------------------------+-------------+-------------+-------------+
  • mutexLocked — 表示互斥锁的锁定状态;
  • mutexWoken — 表示从正常模式被从唤醒;
  • mutexStarving — 当前的互斥锁进入饥饿状态;
  • waitersCount — 当前互斥锁上等待的 goroutine 个数;

2. RWMutex详解

?
1
2
3
4
5
6
7
type RWMutex struct {
    w           Mutex  // 复用互斥锁
    writerSem   uint32 // 写锁监听读锁释放的信号量
    readerSem   uint32 // 读锁监听写锁释放的信号量
    readerCount int32  // 当前正在执行读操作的数量
    readerWait  int32  // 当写操作被阻塞时,需要等待读操作完成的个数
}
  • 读操作如何防止并发读写问题的?

RLock(): 申请读锁,每次执行此函数后,会对readerCount++,此时当有写操作执行Lock()时会判断readerCount>0,就会阻塞。

RUnLock(): 解除读锁,执行readerCount–,释放信号量唤醒等待写操作的goroutine。

  • 写操作如何防止并发读写、并发写写问题?

Lock(): 申请写锁,获取互斥锁,此时会阻塞其他的写操作。并将readerCount 置为 -1,当有读操作进来,发现readerCount = -1, 即知道有写操作在进行,阻塞。

Unlock(): 解除写锁,会先通知所有阻塞的读操作goroutine,然后才会释放持有的互斥锁。

  • 写操作的饥饿问题?

这是由于写操作要等待读操作结束后才可以获得锁,而写操作在等待期间可能还有新的读操作持续到来,如果写操作等待所有读操作结束,很可能会一直阻塞,这种现象称之为写操作被饿死。

通过RWMutex结构体中的readerWait属性可完美解决这个问题。

当写操作到来时,会把RWMutex.readerCount值拷贝到RWMutex.readerWait中,用于标记排在写操作前面的读者个数。

前面的读操作结束后,除了会递减RWMutex.readerCount,还会递减RWMutex.readerWait值,当RWMutex.readerWait值变为0时唤醒写操作。

3. sync.Map详解

一般情况下解决并发读写 map 的思路是加一把大锁,或者把一个 map 分成若干个小 map,对 key 进行哈希,只操作相应的小 map。前者锁的粒度比较大,影响效率;后者实现起来比较复杂,容易出错。

而使用 sync.map 之后,对 map 的读写,不需要加锁。并且它通过空间换时间的方式,使用 read 和 dirty 两个 map 来进行读写分离,降低锁时间来提高效率。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Map struct {
    mu Mutex
    read atomic.Value // readOnly
    dirty map[interface{}]*entry
    misses int
}
 
// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
    m       map[interface{}]*entry
    amended bool // true if the dirty map contains some key not in m.
}
 
type entry struct {
    p unsafe.Pointer // *interface{}
}

Golang的锁机制与使用技巧小结

在进行读操作的时候,会先在read中找,没有命中的话会锁住dirty并且寻找,如果找到了miss计数+1,超过阈值时将dirty赋值给read;

在进行添加操作时,直接在dirty中添加;

在进行修改操作时,先改read,再改dirty;

在进行删除操作时,将read中加上amended标记,dirty中直接删除。

4. 原子操作 atomic.Value

愿此操作的底层是靠 MESI 缓存一致性协议来维持的。

Go的 atomic.Value 需要注意应该放入只读对象。

?
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//atomic.Value源码
 
type Value struct {
    v interface{} // 所以可以存储任何类型的数据
}
 
// 空 interface{} 的内部表示格式,作用是将interface{}类型分解,得到其中两个字段
type ifaceWords struct {
    typ  unsafe.Pointer
    data unsafe.Pointer
}
 
// 取数据就是正常走流程
func (v *Value) Load() (x interface{}) {
    vp := (*ifaceWords)(unsafe.Pointer(v))
    typ := LoadPointer(&vp.typ)
    if typ == nil || uintptr(typ) == ^uintptr(0) {
        // 第一次还没写入
        return nil
    }
  // 构造新的interface{}返回出去
    data := LoadPointer(&vp.data)
    xp := (*ifaceWords)(unsafe.Pointer(&x))
    xp.typ = typ
    xp.data = data
    return
}
 
// 写数据(如何保证数据完整性)
func (v *Value) Store(x interface{}) {
    if x == nil {
        panic("sync/atomic: store of nil value into Value")
    }
  // 绕过 Go 语言类型系统的检查,与任意的指针类型互相转换
    vp := (*ifaceWords)(unsafe.Pointer(v)) // 旧值
    xp := (*ifaceWords)(unsafe.Pointer(&x)) // 新值
    for { // 配合CompareAndSwap达到乐观锁的功效
        typ := LoadPointer(&vp.typ)
        if typ == nil { // 第一次写入
            runtime_procPin() // 禁止抢占
            if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) {
                runtime_procUnpin() // 没有抢到锁,说明已经有别的线程抢先完成赋值,重新进入循环
                continue
            }
            // 首次赋值
            StorePointer(&vp.data, xp.data)
            StorePointer(&vp.typ, xp.typ)
            runtime_procUnpin() // 写入成功,解除占用状态
            return
        }
        if uintptr(typ) == ^uintptr(0) {
            // 第一次写入还未完成,继续等待
            continue
        }
        // 两次需要写入相同类型
        if typ != xp.typ {
            panic("sync/atomic: store of inconsistently typed value into Value")
        }
        StorePointer(&vp.data, xp.data)
        return
    }
}
 
// 禁止抢占,标记当前G在M上不会被抢占,并返回当前所在P的ID。
func runtime_procPin()
// 解除G的禁止抢占状态,之后G可被抢占。
func runtime_procUnpin()

5. 使用小技巧

  • 减小临界区域(减少锁的持有时间)
?
1
2
3
4
5
6
7
8
9
10
11
var m sync.Mutex
 
func DoSth() {
    // do sth1
    func() {
       u.lock()
       defer m.unlock()
       // do sth2
    }()
    // do sth3
}

如上所示,如果do sth3中是很费时的io操作,使用这个技巧可以将临界区减小,提高性能,不过,如果本身临界区就不大,锁操作后续没有什么费时操作,那么也就没有必要这样操作了。

  • 减小锁的粒度

在高并发场景下,用锁的数量来换取并发效率,类似于java中ConcurrentHashmap的分段锁思想,增加锁的数量,减少一把锁控制的数据量。

  • 读写分离(读写锁): RWMutex,sync.Map

在读多写少的情景下,可以使用读写锁,提高读操作的并发性能。

  • 使用原子操作

原子操作是CPU指令级的操作,不会触发g调度机制。,不阻塞执行流

到此这篇关于Golang的锁机制与使用技巧精选的文章就介绍到这了,更多相关Golang 锁机制内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://juejin.cn/post/7103935276564611086

延伸 · 阅读

精彩推荐
  • GolangGo中http超时问题的排查及解决方法

    Go中http超时问题的排查及解决方法

    这篇文章主要介绍了Go中http超时问题的排查及解决方法,本文给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下 ...

    蘑菇先生5512020-05-29
  • Golang浅析goland等待锁问题

    浅析goland等待锁问题

    这篇文章主要介绍了goland等待锁问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧...

    Skyego9562021-02-01
  • Golang一篇带给你Go语言的并发

    一篇带给你Go语言的并发

    并行指的是在同一时间,多个程序在不同的 CPU 上共同运行,互相之间并没有对 CPU 资源进行竞争。比如,我在看书的时候,左手用来翻书,右手做笔记,两...

    自然醒的笔记本3662021-06-24
  • GolangGolang导入包的几种方式(点,别名与下划线)

    Golang导入包的几种方式(点,别名与下划线)

    这篇文章主要介绍了Golang导入包的几种方式(点,别名与下划线),文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    头秃猫轻王9772022-09-04
  • Golanggolang 定时任务方面time.Sleep和time.Tick的优劣对比分析

    golang 定时任务方面time.Sleep和time.Tick的优劣对比分析

    这篇文章主要介绍了golang 定时任务方面time.Sleep和time.Tick的优劣对比分析,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    Star_CSU4492021-06-18
  • GolangGo语言的JSON处理详解

    Go语言的JSON处理详解

    json格式可以算我们日常最常用的序列化格式之一了,Go语言作为一个由Google开发,号称互联网的C语言的语言,自然也对JSON格式支持很好。 ...

    冷月醉雪2702020-05-20
  • Golang再次探讨go实现无限 buffer 的 channel方法

    再次探讨go实现无限 buffer 的 channel方法

    我们知道go语言内置的channel缓冲大小是有上限的,那么我们自己如何实现一个无限 buffer 的 channel呢?今天通过本文给大家分享go实现无限 buffer 的 channel方法...

    机智的小小帅5482021-08-08
  • Golang对Golang import 导入包语法详解

    对Golang import 导入包语法详解

    今天小编就为大家分享一篇对Golang import 导入包语法详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧 ...

    独一无二的小个性5392020-05-26