Mutex使用

Mutex

Mutex 互斥锁主要用来解决高并发的访问问题,常见的并发场景有:

  • 多个 goroutine 并发更新同一个资源,像计数器;
  • 同时更新用户的账户信息;
  • 秒杀系统;往同一个 buffer 中并发写入数据等等。
    如果没有互斥控制,就会出现一些异常情况,比如计数器的计数不准确、用户的账户可能出现透 支、秒杀系统出现超卖、buffer 中的数据混乱等等。

互斥锁的机制

互斥锁是并发控制的一个基本手段,是为了避免竞争而建立的一种并发控制机制。要了解互斥锁就先搞懂临界区的概念。

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

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

如果多个并发线程同时访问临界区,就会造成访问或者操作错误,此时就要用互斥锁,限定临界区只能同时由一个线程持有。

Mutex 基本用法

Mutex 实现了go的标准库 package sync 的Locker接口。
Locker 定义:

// A Locker represents an object that can be locked and unlocked.
type Locker interface {
    Lock()
    Unlock()
}

进入临界区(共享资源)之前调用Lock方法,退出临界区的时候调用Unlock方法。

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

引用一个并发计数的示例来说明mutex的使用:
创建10 个 goroutine 不断多一个counter操作加1,期望计数的结果是 10 * 100000 = 1000000 (一百万)。
代码如下:

package main

import (
	"fmt"
	"sync"
)

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

如果不加Mutex的时候,每次结果都不会是期望结果的 1000000

➜ mutex (master) ✗ go run counter.go                         
37727
➜ mutex (master) ✗ go run counter.go
37426
➜ mutex (master) ✗ go run counter.go
40452

这是因为count++ 每次都不是原子操作并会有并发+1的情况。

go 官方提供了一个检测此类并发访问共享资源是否有问题的工具 data race;
如果出现并发问题就会打印竞争警告信息,使用方法为go run --race counter.go

➜ mutex (master) ✗ go run --race counter.go 
==================
WARNING: DATA RACE
Read at 0x00c00013c018 by goroutine 8:
  main.main.func1()
      /Users/lsrong/Work/Project/Go/src/github.com/lsrong/gotraining/concurrency/mutex/counter.go:16 +0x84

Previous write at 0x00c00013c018 by goroutine 7:
  main.main.func1()
      /Users/lsrong/Work/Project/Go/src/github.com/lsrong/gotraining/concurrency/mutex/counter.go:16 +0x98

Goroutine 8 (running) created at:
  main.main()
      /Users/lsrong/Work/Project/Go/src/github.com/lsrong/gotraining/concurrency/mutex/counter.go:13 +0xc4

Goroutine 7 (finished) created at:
  main.main()
      /Users/lsrong/Work/Project/Go/src/github.com/lsrong/gotraining/concurrency/mutex/counter.go:13 +0xc4
==================
50021
Found 1 data race(s)
exit status 66

除了运行时加入 --race 参数,运行结果最后显示Found 1 data race(s)发现一个数据竞争问题(并发问题),除了在go run运行是检测竞争问题,还有可以运行 go tool compile -race -S counter.go, 查看编译后的代码,也可以发现竞争问题,如下:

        ......
        0x0024 00036 (counter.go:8)     PCDATA  $1, ZR
        0x0024 00036 (counter.go:8)     CALL    runtime.racefuncenter(SB)
        0x0028 00040 (counter.go:9)     MOVD    $type.int(SB), R0
        0x0030 00048 (counter.go:9)     MOVD    R0, 8(RSP)
        0x0034 00052 (counter.go:9)     CALL    runtime.newobject(SB)
        0x0038 00056 (counter.go:9)     MOVD    16(RSP), R0
        0x003c 00060 (counter.go:9)     MOVD    R0, "".&counter-24(SP)
        0x0040 00064 (counter.go:9)     MOVD    R0, 8(RSP)
        0x0044 00068 (counter.go:9)     PCDATA  $1, $1
        0x0044 00068 (counter.go:9)     CALL    runtime.racewrite(SB)
        0x0048 00072 (counter.go:9)     MOVD    "".&counter-24(SP), R0
        0x004c 00076 (counter.go:9)     MOVD    ZR, (R0)
        0x0050 00080 (counter.go:10)    MOVD    $type.sync.WaitGroup(SB), R1
        0x0058 00088 (counter.go:10)    MOVD    R1, 8(RSP)
        ......

如果出现data race Go 编译器会加上 runtime.racefuncenter(SB) runtime.racewrite(SB) 等检测 data race的方法,这些指令用于 Go race detector工具能够检测出data race 的问题。

接下来就是就此类并发问题就可以使用Mutex来处理,在进入临界区 count++ 的时候获得锁,在离开临界区的时候释放锁.
代码如下:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var counter = 0
	var wg sync.WaitGroup
	var mu sync.Mutex
	wg.Add(10)
	for i := 0; i < 10; i++ {
		// 并发 加1
		go func() {
			defer wg.Done()
			for j := 0; j < 10000; j++ {
				// 加锁
				mu.Lock()
				counter++
				mu.Unlock()
			}
		}()
	}
	wg.Wait()
	fmt.Println(counter)
}

此时运行结果就和预期的一致了,同时 data race 也没有警告信息。

➜ mutex (master) ✗ go run --race  mutexcounter.go
100000

Mutex 嵌入到struct中

有时候还可以将Mutex嵌入到自定义的stuct中,比如:

type Counter struct {
	mu sync.Mutex
	count uint64
}

这样初始化的时候就不用初始化Mutex变量,还可以把获取锁、释放锁、计数加1的逻辑封装成一个方法,对外不暴露锁等逻辑。
代码如下:

package main

import (
	"fmt"
	"sync"
)

type Counter struct {
	mu    sync.Mutex
	count uint64
}

func (c *Counter) Incr() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.count++
}

func (c *Counter) Count() uint64 {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.count
}

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())
}

运行结果如下:

➜ mutex (master) ✗ go run --race mutexcounter_struct.go
1000000

总结

本文主要学习Mutex的使用和实践,通过race detector工具发现并发问题,如果开发过程中也会遇到有类似并发的问题,都可以考虑用通过互斥锁的手段去解决。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值