[Go 并发编程实战课]01.Mutex 学习笔记

用户头像
custer
关注
发布于: 2020 年 10 月 14 日
[Go并发编程实战课]01.Mutex学习笔记

学习目标

  1. 了解互斥锁的实现机制

  2. Go 标准库的互斥锁 Mutex 基本使用方法

  3. Mutex 具体实现原理、易错场景和拓展用法

互斥锁的实现机制

互斥锁是并发控制的一个基本手段,是为了避免竞争而建立的一种并发控制机制。

临界区

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



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



Mutex 的基本使用方法

Mutex 实现了 Locker 接口

Locker 接口定义了锁同步原语的方法集

type Locker interface {
Lock() // 请求锁
Unlock() // 释放锁
}

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

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

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

不加锁的 Demo

package main
import (
"fmt"
"sync"
)
func main() {
var count = 0
// 使用 waitgroup 等待10 个 goroutine 完成
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
// 对变量 count 执行 100000 次加 1
for j := 0; j < 100000; j++ {
count++
}
}()
}
// 等待 10 个 goroutine 完成
wg.Wait()
fmt.Println(count)
}

每次运行都得到不同的结果,基本上不会得到理想中的结果 一百万。

因为 count++ 不是一个原子操作,它只是包含了读取变量 count 的当前值,对这个值加 1,把结果再保存到 count 中。因为不是原子操作,就有可能有并发问题。

使用 go run -race main.go 可以检测一下是不是有并发问题。

使用互斥锁 Demo

package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex // 互斥锁保护计数器
var count = 0
// 使用 waitgroup 等待10 个 goroutine 完成
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
// 对变量 count 执行 100000 次加 1
for j := 0; j < 100000; j++ {
mu.Lock()
count++
mu.Unlock()
}
}()
}
// 等待 10 个 goroutine 完成
wg.Wait()
fmt.Println(count)
}

再运行,data race 没有警告,运行结果也是 1000000。

互斥锁的最佳实践 Demo

Mutex 嵌入到其他 struct 中使用

package main
import (
"fmt"
"sync"
)
func main() {
var counter Counter
// 使用 waitgroup 等待10 个 goroutine 完成
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
// 对变量 count 执行 100000 次加 1
for j := 0; j < 100000; j++ {
counter.Lock()
counter.Count++
counter.Unlock()
}
}()
}
// 等待 10 个 goroutine 完成
wg.Wait()
fmt.Println(counter.Count)
}
type Counter struct {
sync.Mutex
Count uint64
}

互斥锁封装成方法 Demo

可以把 获取锁、释放锁、计数加一的逻辑封装成一个方法,对外不需要暴露锁等逻辑。

如果嵌入的 strcut 有多个字段,一般把 Mutex 放在要控制的字段上面,然后使用空格把字段分隔开来。

package main
import (
"fmt"
"sync"
)
func main() {
var counter Counter // 封装好的计数器
// 使用 waitgroup 等待10 个 goroutine 完成
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ { // 启动10个goroutine
go func() {
defer wg.Done()
// 对变量 count 执行 10万 次加 1
for j := 0; j < 100000; j++ {
counter.Incr() //
}
}()
}
// 等待 10 个 goroutine 完成
wg.Wait()
fmt.Println(counter.Count())
}
// 线程安全的计数器类型
type Counter struct {
CounterType int
Name string
mu sync.Mutex
count uint64
}
// 加 1 的方法,内部使用互斥锁保护
func (c *Counter) Incr() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
// 得到计数器的值,也需要锁保护
func (c *Counter) Count() uint64 {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}

思考

如果 Mutex 已经被一个 goroutine 获取了锁,其它等待中的 goroutine 们只能一直等待。那么,等这个锁释放后,等待中的 goroutine 中哪一个会优先获取 Mutex 呢?



鸟窝:sync.mutex 源代码分析



用户头像

custer

关注

让正确的事情连续发生 2017.11.20 加入

还未添加个人简介

评论

发布
暂无评论
[Go并发编程实战课]01.Mutex学习笔记