Go并发2 同步原语 - Mutex 的使用

155 阅读5分钟

并发问题
我们知道多线程的目的是为了提高效率,充分利用CPU资源,从而减少任务执行时间,但是也会带来共享资源的并发问题。

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

临界区的特点
临界区就是一个被共享的资源,或者说是一个整体的一组共享资源,比如对数据库的访问、对某一个共享数据结构的操作、对一个 I/O 设备的使用、对一个连接池中的连接的调用等,这些标准库都是为了解决共享资源竞争的问题。

互斥锁
为了限定临界区只能同时由一个线程持有,我们会使用互斥锁对临界区进行lock和unlock操作。 很多语言提供了互斥锁的标准库,比如java的synchronized和Lock, Go的Mutex, RWMutex(读写锁)。

协程互斥访问临界资源的过程

image.png

Mutex
在Go的标准库中,package sync 提供了锁相关的一系列同步原语,而Mutex就实现了这个 package中的Locker接口。

Locker接口

type Locker interface {
 Lock()
 Unlock()
}

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

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

如何发现Go程序中的非并发安全操作
Go原生提供了一个race工具,可以在compile,run,test的过程中检测出data race问题。

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

counter.go

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执行10次加1
         for j := 0; j < 100000; j++ {
            count++
         }
      }()
   }
   // 等待10个goroutine完成
   wg.Wait()
   fmt.Println(count)
}

Go trace的使用

PS D:\Terry\GoSync> go run .\src\mymutex\my_mutex.go
382856
PS D:\Terry\GoSync> go run -race .\src\mymutex\my_mutex.go
==================
WARNING: DATA RACE       
goroutine 8会有读操作
Read at 0x00c00012c078 by goroutine 8:
  main.main.func1()                   
      D:/Terry/GoSync/src/mymutex/my_mutex.go:18 +0xa8
goroutine 7会有写操作
Previous write at 0x00c00012c078 by goroutine 7:
  main.main.func1()
      D:/Terry/GoSync/src/mymutex/my_mutex.go:18 +0xba

Goroutine 8 (running) created at:
  main.main()
      D:/Terry/GoSync/src/mymutex/my_mutex.go:14 +0x8d

Goroutine 7 (running) created at:
  main.main()
      D:/Terry/GoSync/src/mymutex/my_mutex.go:14 +0x8d
==================
350900
Found 1 data race(s)

Data trace的原理
通过在编译期插入runtime.racefuncenter、runtime.raceread、runtime.racewrite、runtime.racefuncexit 等检测 data race 的方法,在运行期运行这些指令,从而实现检测data race的功能。

go tool compile -race -S counter.go

0x002a 00042 (counter.go:13) CALL runtime.racefuncenter(SB)
 ......
 0x0061 00097 (counter.go:14) JMP 173
 0x0063 00099 (counter.go:15) MOVQ AX, "".j+8(SP)
 0x0068 00104 (counter.go:16) PCDATA $0, $1
 0x0068 00104 (counter.go:16) MOVQ "".&count+128(SP), AX
 0x0070 00112 (counter.go:16) PCDATA $0, $0
 0x0070 00112 (counter.go:16) MOVQ AX, (SP)
 0x0074 00116 (counter.go:16) CALL runtime.raceread(SB)
 0x0079 00121 (counter.go:16) PCDATA $0, $1
 0x0079 00121 (counter.go:16) MOVQ "".&count+128(SP), AX
 0x0081 00129 (counter.go:16) MOVQ (AX), CX
 0x0084 00132 (counter.go:16) MOVQ CX, ""..autotmp_8+16(SP)
 0x0089 00137 (counter.go:16) PCDATA $0, $0
 0x0089 00137 (counter.go:16) MOVQ AX, (SP)
 0x008d 00141 (counter.go:16) CALL runtime.racewrite(SB)
 0x0092 00146 (counter.go:16) MOVQ ""..autotmp_8+16(SP), AX
 ......
 0x00b6 00182 (counter.go:18) CALL runtime.deferreturn(SB)
 0x00bb 00187 (counter.go:18) CALL runtime.racefuncexit(SB)
 0x00c0 00192 (counter.go:18) MOVQ 104(SP), BP
 0x00c5 00197 (counter.go:18) ADDQ $112, SP

Mutex的使用
直接使用
Mutex 的零值是还没有 goroutine 等待的未加锁的状态,所以不需要额外的初始化,直接声明变量(如 var mu sync.Mutex)

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

   // 辅助变量,用来确认所有的goroutine都完成
   var wg sync.WaitGroup
   wg.Add(10)
   // 启动10个gourontine
   for i := 0; i < 10; i++ {
      go func() {
         defer wg.Done()
         // 累加10万次
         for j := 0; j < 100000; j++ {
            mu.Lock()
            //临界区
            count++
            mu.Unlock()
         }
      }()
   }
   wg.Wait()
   fmt.Println(count)
}

嵌入到其他struct使用
在初始化嵌入的 struct 时,也不必初始化这个 Mutex 字段,不会因为没有初始化出现空指针或者是无法获取到锁的情况

type Counter struct {
 mu sync.Mutex
 Count uint64
}

嵌入字段方式使用
通过嵌入字段,可以在这个 struct 上直接调用 Lock/Unlock 方法。如果嵌入的 struct 有多个字段,建议将 Mutex 放在要控制的字段上面,然后使用空格把字段分隔开来,用这种风格去写的话,逻辑会更清晰,也更易于维护。

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

封装使用 我们还可以把获取锁、释放锁、计数加一的逻辑封装成一个方法(类似Inc/Dec原子方法),对外不需要暴露锁等逻辑。

func main() {
   // 封装好的计数器
   var counter Counter
   var wg sync.WaitGroup
   wg.Add(10)
   // 启动10个goroutine
   for i := 0; i < 10; i++ {
      go func() {
         defer wg.Done()
         // 执行10万次累加
         for j := 0; j < 100000; j++ {
            counter.Incr() // 受到锁保护的方法
         }
      }()
   }
   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
}