01 | Mutex:如何解决资源并发访问问题?

01-Mutex:如何解决资源并发访问问题

互斥锁的实现机制

临界区

在并发编程中,如果程序中的一部分会被并发访问或修改,那么,为了避免并发访问导致的意想不到的结果,这部分程序需要被保护起来,这部分被保护起来的程序,就叫做临界区。

临界区就是一个被共享的资源,或者说是一个整体的一组共享资源,比如对数据库的访问、对某一个共享数据结构的操作、对一个 I/O 设备的使用、对一个连接池中的连接的调用,等等。

多个线程同步访问临界区,就会造成访问或操作错误。

使用互斥锁,限定临界区只能为一个线程持有。

image-20230110172308798

Mutex是使用最广泛的同步原语,也叫并发原语

同步原语的使用场景:

  • 共享资源:数据竞争,常用Mutex、RWMutex
  • 任务编排:goroutine按照一定规律执行,常用WaitGroup、Channel
  • 消息传递:不同的goroutine之间线程安全交流,常用Channel

Mutex的基本使用方法

Locker接口

1
2
3
4
type Locker interface{
Lock()
Unlock()
}

互斥锁Mutex提供两个方法Lock和Unlock:进入临界区前调用Lock,退出临界区调用Unlock

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

当一个 goroutine 通过调用 Lock 方法获得了这个锁的拥有权后, 其它请求锁的 goroutine就会阻塞在 Lock 方法的调用上,直到锁被释放并且自己获取到了这个锁的拥有权。

为什么要加锁?

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 (
"fmt"
"sync"
)

func main() {
// 计数器的值
var count = 0

// 辅助变量,用来确定所有goroutine都完成
var wg sync.WaitGroup
wg.Add(10)

// 启动10个goroutine
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
count++
}
}()
}
wg.Wait()
fmt.Println(count)
}

执行结果不尽如人意啊!!!

image-20230110165713065

cout++不是一个原子操作!!好多计数被吞掉了

race detector

编译器通过探测所 有的内存访问,加入代码能监视对这些内存地址的访问(读还是写)。在代码运行的时候,race detector 就能监控到对共享变量的非同步访问,出现 race 的时候,就会打印出警告信息。

1
go run -race example1.go

但是只能通过真正对实际地址进行读写访问的时候才能探测,所以它并不能在编译的时候发现 data race 的问题。

image-20230110165833018

运行go tool compile -race -S example1.go,可以查看计数器例子的代码

image-20230110193751324

使用方法

临界区是count++,只要在临界区前面获取锁,在离开临界区的时候释放锁,就可以完美解决data race问题

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
package main

import (
"fmt"
"sync"
)

func main() {
// 互斥锁保护计时器
var mu sync.Mutex
// 计数器的值
var count = 0

// 辅助变量,用来确定所有goroutine都完成
var wg sync.WaitGroup
wg.Add(10)

// 启动10个goroutine
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
mu.Lock()
for j := 0; j < 1000; j++ {
count++
}
mu.Unlock()
}()
}
wg.Wait()
fmt.Println(count)
}

其他用法:

嵌入字段

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
package main

import (
"fmt"
"sync"
)

// Mutex 嵌入字段使用
type Counter1 struct {
sync.Mutex
count uint64
}

func main() {
var counter Counter1
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
counter.Lock()
counter.count++
counter.Unlock()
}

}()
}
wg.Wait()
fmt.Println(counter.count)
}

将获取锁、释放锁、计数加一的逻辑封装成一个方法,不对外暴露锁逻辑

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
package main

import (
"fmt"

"sync"
)

func main() {
var counter Counter2
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
counter.Incr()
}
}()
}
wg.Wait()
fmt.Println(counter.Count())
}

// 线性安全的计数器形式
type Counter2 struct {
CounterType int
Name string
mu sync.Mutex
count uint64
}

// 加1的方法,内部使用互斥锁保护
func (counter *Counter2) Incr() {
counter.mu.Lock()
counter.count++
counter.mu.Unlock()
}

//得到计数器的值,也需要互斥锁的保护
func (counter *Counter2) Count() uint64 {
counter.mu.Lock()
defer counter.mu.Unlock()
return counter.count
}


01 | Mutex:如何解决资源并发访问问题?
http://example.com/2023/01/10/01-Mutex:如何解决资源并发访问问题/
Author
WYX
Posted on
January 10, 2023
Licensed under