【Golang】 关于Go语言中的锁

在 Golang 里有专门的方法来实现锁,就是 sync 包,这个包有两个很重要的锁类型

一个叫 Mutex, 利用它可以实现互斥锁。一个叫 RWMutex,利用它可以实现读写锁。

特别说明:

  • sync.Mutex 的锁是不可以嵌套使用的
  • sync.RWMutex 的 RLock()是可以嵌套使用的
  • sync.RWMutex 的 mu.Lock() 是不可以嵌套的
  • sync.RWMutex 的 mu.Lock() 中不可以嵌套 mu.RLock()

否则,会 panic fatal error: all goroutines are asleep - deadlock!

一、实例说明

package main

import (
	"sync"
	"time"
)

var l sync.RWMutex

func readAndRead() { // 可读锁内使用可读锁
	l.RLock()
	defer l.RUnlock()

	l.RLock()
	defer l.RUnlock()
}

func lockAndLock() { // 全局锁内使用全局锁
	l.Lock()
	defer l.Unlock()

	l.Lock()
	defer l.Unlock()
}

func lockAndRead() { // 全局锁内使用可读锁
	l.Lock()
	defer l.Unlock() // 由于 defer 是栈式执行,所以这两个锁是嵌套结构

	l.RLock()
	defer l.RUnlock()
}

func readAndLock() { // 可读锁内使用全局锁
	l.RLock()
	defer l.RUnlock()

	l.Lock()
	defer l.Unlock()
}
func main() {
	readAndRead()
	readAndLock()

	lockAndRead()
	lockAndLock()

	time.Sleep(5 * time.Second)
}

二、 互斥锁 :Mutex

使用互斥锁(Mutex,全称 mutual exclusion)是为了来保护一个资源不会因为并发操作而引起冲突导致数据不准确。下面这段代码开启了三个协程,每个协程分别往 count 这个变量加10000次 ,理论上看 count 值应试为 30000

package main

import (
	"fmt"
	"sync"
)

func add(count *int, wg *sync.WaitGroup) {
	for i := 0; i < 10000; i++ {

		*count = *count + 1

	}
	wg.Done()
}

func main() {
	var wg sync.WaitGroup
	count := 0
	wg.Add(3)
	go add(&count, &wg)
	go add(&count, &wg)
	go add(&count, &wg)

	wg.Wait()
	fmt.Println("count 的值为:", count)
}

执行的结果为:

PS E:\project\demo> go run test6.go
count 的值为: 18186
PS E:\project\demo> go run test6.go
count 的值为: 19154
PS E:\project\demo> go run test6.go
count 的值为: 23215

原因就在于这三个协程在执行时,先读取 count 再更新 count 的值,而这个过程并不具备原子性,所以导致了数据的不准确。解决这个问题的方法,就是给 add 这个函数加上 Mutex 互斥锁,要求同一时刻,仅能有一个协程能对 count 操作。在写代码前,先了解一下 Mutex 锁的两种定义方法

然后修改代码,如下所示

import (
    "fmt"
    "sync"
)

func add(count *int, wg *sync.WaitGroup, lock *sync.Mutex) {
    for i := 0; i < 10000; i++ {
        lock.Lock()
        *count = *count + 1
        lock.Unlock()
    }
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    lock := &sync.Mutex{}

    //或者
    //var lock *sync.Mutex
    //lock = new(sync.Mutex)

    count := 0
    wg.Add(3)
    go add(&count, &wg, lock)
    go add(&count, &wg, lock)
    go add(&count, &wg, lock)

    wg.Wait()
    fmt.Println("count 的值为:", count)
}

不管你执行多少次,输出都只有一个结果,count 的值为: 30000

使用 Mutext 锁虽然很简单,但仍然有几点需要注意:

  • 同一协程里,不要在尚未解锁时再次使加锁

  • 同一协程里,不要对已解锁的锁再次解锁

  • 加了锁后,别忘了解锁,必要时使用 defer 语句 

三、读写锁:RWMutex

Mutex 是最简单的一种锁类型,他提供了一个傻瓜式的操作,加锁解锁加锁解锁,让你不需要再考虑其他的。简单 同时意味着在某些特殊情况下有可能会造成时间上的浪费,导致程序性能低下。

  • 为了保证数据的安全,它规定了当有人还在读取数据(即读锁占用)时,不允计有人更新这个数据(即写锁会阻塞)

  • 为了保证程序的效率,多个人(线程)读取数据(拥有读锁)时,互不影响不会造成阻塞,它不会像 Mutex 那样只允许有一个人(线程)读取同一个数据。

理解了这个后,再来看看,如何使用 RWMutex?

定义一个 RWMuteux 锁,同样有两种方法

RWMutex 里提供了两种锁,每种锁分别对应两个方法,为了避免死锁,两个方法应成对出现,必要时请使用 defer。

  • 读锁:调用 RLock 方法开启锁,调用 RUnlock 释放锁

  • 写锁:调用 Lock 方法开启锁,调用 Unlock 释放锁(和 Mutex类似)

接下来,直接看一下例子吧 

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {

    //var lock *sync.RWMutex
    //lock = new(sync.RWMutex)
    //或者

    lock := &sync.RWMutex{}
    lock.Lock()

    for i := 0; i < 4; i++ {
        go func(i int) {
            fmt.Printf("第 %d 个协程准备开始... \n", i)
            lock.RLock()
            fmt.Printf("第 %d 个协程获得读锁, sleep 1s 后,释放锁\n", i)
            time.Sleep(time.Second)
            lock.RUnlock()
        }(i)
    }

    time.Sleep(time.Second * 2)

    fmt.Println("准备释放写锁,读锁不再阻塞")
    // 写锁一释放,读锁就自由了
    lock.Unlock()

    // 由于会等到读锁全部释放,才能获得写锁
    // 因为这里一定会在上面 4 个协程全部完成才能往下走
    lock.Lock()
    fmt.Println("程序退出...")
    lock.Unlock()
}

执行结果如下

PS E:\project\demo> go run test8.go
第 0 个协程准备开始...
第 3 个协程准备开始...
第 1 个协程准备开始...
第 2 个协程准备开始...
准备释放写锁,读锁不再阻塞
第 2 个协程获得读锁, sleep 1s 后,释放锁
第 3 个协程获得读锁, sleep 1s 后,释放锁
第 0 个协程获得读锁, sleep 1s 后,释放锁
第 1 个协程获得读锁, sleep 1s 后,释放锁
程序退出...

四、自动检测死锁deadlock

package main

import (
	"fmt"
	"sync"
	"time"
	"github.com/sasha-s/go-deadlock"
)

var (
	mu1 deadlock.Mutex
	mu2 deadlock.Mutex
	wg sync.WaitGroup
)

func main() {
	wg.Add(2)

	go func() {
		mu1.Lock()
		time.Sleep(1 * time.Second)
		mu2.Lock()
	}()

	go func() {
		mu2.Lock()
		mu1.Lock()
	}()

	go func() {
		for {
			time.Sleep(1 * time.Second)
			fmt.Println("test")
		}
	}()

	wg.Wait()
}

运行结果如下

POTENTIAL DEADLOCK: Inconsistent locking. saw this ordering in one goroutine:
test
happened before
..\pkg\mod\github.com\sasha-s\go-deadlock@v0.3.1\deadlock.go:85 go-deadlock.(*Mutex).Lock { lock(m.mu.Lock, m) } <<<<<
test9.go:26 main.main.func2 { mu2.Lock() }

happened after
..\pkg\mod\github.com\sasha-s\go-deadlock@v0.3.1\deadlock.go:85 go-deadlock.(*Mutex).Lock { lock(m.mu.Lock, m) } <<<<<
test9.go:27 main.main.func2 { mu1.Lock() }

in another goroutine: happened before
..\pkg\mod\github.com\sasha-s\go-deadlock@v0.3.1\deadlock.go:85 go-deadlock.(*Mutex).Lock { lock(m.mu.Lock, m) } <<<<<
test9.go:20 main.main.func1 { mu1.Lock() }

happened after
..\pkg\mod\github.com\sasha-s\go-deadlock@v0.3.1\deadlock.go:85 go-deadlock.(*Mutex).Lock { lock(m.mu.Lock, m) } <<<<<
test9.go:22 main.main.func1 { mu2.Lock() }

Other goroutines holding locks:
goroutine 19 lock 0x5d6ea8
..\pkg\mod\github.com\sasha-s\go-deadlock@v0.3.1\deadlock.go:85 go-deadlock.(*Mutex).Lock { lock(m.mu.Lock, m) } <<<<<
test9.go:20 main.main.func1 { mu1.Lock() }



exit status 2 

在多场景下go-deadlock如何做的死锁检测 ?

场景1:当协程1拿到了lock1的锁,然后再尝试拿lock1锁?

很简单,用一个map存入所有为释放锁的协程id, 当检测到gid相同时, 触发OnPotentialDeadlock回调方法。如果拿到一个锁,又通过 go func()去拿同样的锁,这时候就无法快速检测死锁了,只能依赖go-deadlock提供了锁超时检测。

场景2:协程1拿到了lock1, 协程2拿到了lock2, 这时候协程1再去拿lock2, 协程2尝试去拿lock1

这是交叉拿锁引起的死锁问题,如何解决? 我们可以存入beferAfter关系。在go-deadlock里有个order map专门来存这个关系。当协程1再去拿lock2的时候, 如果order里有 lock1-lock2, 那么触发OnPotentialDeadlock回调方法。

场景3:如果协程1拿到了lock1,但是没有写unlock方法,协程2尝试拿lock1, 会一直阻塞的等待

go deadlock会针对开启DeadlockTimeout >0 的加锁过程,new一个协程来加入定时器判断是否锁超时。

 

posted @ 2021-10-03 12:48  踏雪无痕SS  阅读(2023)  评论(0编辑  收藏  举报