关于go:Go并发编程发生死锁活锁的案例分析

5次阅读

共计 4301 个字符,预计需要花费 11 分钟才能阅读完成。

什么是死锁、活锁

什么是死锁:就是在并发程序中,两个或多个线程彼此期待对方实现操作,从而导致它们都被阻塞,并无限期地期待对方实现。这种状况下,程序会卡死,无奈继续执行。

什么是活锁:就是程序始终在运行,然而无奈获得停顿。例如,在某些状况下,多个线程会抢夺同一个资源,而后每个线程都会开释资源,以便其余线程能够应用它。然而,如果没有正确的同步,这些线程可能会同时尝试获取该资源,而后再次开释它。这可能导致线程在有限循环中运行,却无奈获得停顿。

产生死锁的案例剖析

  1. 编写会产生死锁的代码:
package main

import (
 "fmt"
 "sync"
)

func main() {
 var mu sync.Mutex
 mu.Lock()
 defer mu.Unlock()

 wg := sync.WaitGroup{}
 wg.Add(1)
 go func() {fmt.Println("goroutine started")
  mu.Lock() // 在这里获取了锁
  fmt.Println("goroutine finished")
  mu.Unlock()
  wg.Done()}()

 wg.Wait()}

运行和输入:

[root@workhost temp02]# go run main.go 
goroutine started
fatal error: all goroutines are asleep - deadlock! # 谬误很显著了,通知你死锁啦!goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000010030?)
        /usr/local/go/src/runtime/sema.go:62 +0x27
...
...

下面的代码,应用 sync.Mutex 实现了一个互斥锁。主 goroutine 获取了锁,并启动了一个新的 goroutine。新 goroutine 也尝试获取锁来执行其工作。然而,因为主 goroutine 没有开释锁,新 goroutine 将始终期待锁,导致死锁。

  1. 代码革新

在下面的代码中,能够通过将主 goroutine 中的 defer mu.Unlock() 移到 goroutine 函数中的 mu.Unlock() 前面来解决问题。这样,当 goroutine 获取到锁后,它能够在实现工作后开释锁,以便主 goroutine 能够继续执行。

革新后的代码:

package main

import (
 "fmt"
 "sync"
)

func main() {
 var mu sync.Mutex
 mu.Lock()
 wg := sync.WaitGroup{}
 wg.Add(1)
 go func() {fmt.Println("goroutine started")
  mu.Lock() // 在这里获取了锁
  fmt.Println("goroutine finished")
  mu.Unlock()
  wg.Done()}()
 mu.Unlock() // 开释锁
 wg.Wait()}

运行和输入:

[root@workhost temp02]# go run main.go 
goroutine started
goroutine finished
  1. 如何防止死锁

在 Go 语言中,要防止死锁,肯定要分明以下几个规定:

  • 防止嵌套锁:在应用多个锁时,确保它们的嵌套程序雷同。否则,可能会呈现循环期待的状况,导致死锁。
  • 防止有限期待:如果在获取锁时指定了超时工夫,确保在超时后可能处理错误或执行其余操作。
  • 防止适度竞争:如果多个协程须要拜访雷同的资源,请确保它们不会相互烦扰。能够应用互斥锁或读写锁等机制来解决竞争问题。
  • 应用通道:Go 语言中的通道能够用于协调并发操作。应用通道来传递音讯和同步操作,能够防止死锁和竞争问题。
  • 确保资源开释:在应用锁或其余资源时,肯定要确保它们在应用后失去开释,否则可能会导致死锁。
  • 应用 select 语句:在应用通道进行并发操作时,能够应用 select 语句来防止死锁。通过 select 语句抉择多个通道中的一个进行操作,能够防止在某个通道被阻塞时呈现死锁。

发生存锁的案例剖析

  1. 编写会发生存锁的代码:
package main

import (
 "fmt"
 "sync"
)

func main() {
 var wg sync.WaitGroup
 var mu sync.Mutex
 var flag bool

 wg.Add(2)

 // goroutine 1
 go func() {
  // 先获取锁资源
  fmt.Println("goroutine 1 获取 mu")
  mu.Lock()
  defer mu.Unlock()

  // 而后期待 flag 变量的值变为 true
  fmt.Println("goroutine 1 期待标记")
  for !flag {// 一直循环期待}

  // 最终输入并开释锁资源
  fmt.Println("goroutine 1 从期待中开释")
  wg.Done()}()

 // goroutine 2
 go func() {
  // 先获取锁资源
  fmt.Println("goroutine 2 获取 mu")
  mu.Lock()
  defer mu.Unlock()

  // 而后期待 flag 变量的值变为 true
  fmt.Println("GoRoutine2 期待标记")
  for !flag {// 一直循环期待}

  // 最终输入并开释锁资源
  fmt.Println("GoRoutine 2 从期待中开释")
  wg.Done()}()

 // 在主线程中期待 1 秒钟,以便两个 goroutine 开始期待 flag 变量的值
 // 而后将 flag 变量设置为 true
 // 因为两个 goroutine 会同时唤醒并尝试获取锁资源,它们会互相期待
 // 最终导致了活锁问题,它们都无奈向前推动
 fmt.Println("主线程休眠 1 秒")
 fmt.Println("两个 goroutine 都应该期待标记")
 flag = true
 wg.Wait()

 fmt.Println("所有 GoRoutines 已实现")
}

运行和输入:

[root@workhost temp02]# go run main.go 
主线程休眠 1 秒
两个 goroutine 都应该期待标记
goroutine 2 获取 mu
GoRoutine2 期待标记
GoRoutine 2 从期待中开释
goroutine 1 获取 mu
goroutine 1 期待标记
goroutine 1 从期待中开释
所有 GoRoutines 已实现 

下面的代码存在活锁问题。如果两个 goroutine 同时期待 flag 变为 true 并且都曾经获取了锁资源,那么它们就会进入一个死循环并互相期待,无奈持续向前推动。

  1. 代码革新

革新后的代码:

package main

import (
 "fmt"
 "runtime"
 "sync"
)

func main() {
 var wg sync.WaitGroup
 var mu sync.Mutex
 var flag bool

 wg.Add(2)

 // goroutine 1
 go func() {
  // 先获取锁资源
  fmt.Println("goroutine 1 获取 mu")
  mu.Lock()
  defer mu.Unlock()

  // 而后期待 flag 变量的值变为 true
  fmt.Println("goroutine 1 期待标记")
  for !flag {runtime.Gosched() // 让出工夫片
  }

  // 最终输入并开释锁资源
  fmt.Println("goroutine 1 从期待中开释")
  wg.Done()}()

 // goroutine 2
 go func() {
  // 先获取锁资源
  fmt.Println("goroutine 2 获取 mu")
  mu.Lock()
  defer mu.Unlock()

  // 而后期待 flag 变量的值变为 true
  fmt.Println("GoRoutine2 期待标记")
  for !flag {runtime.Gosched() // 让出工夫片
  }

  // 最终输入并开释锁资源
  fmt.Println("GoRoutine 2 从期待中开释")
  wg.Done()}()

 // 在主线程中期待 1 秒钟,以便两个 goroutine 开始期待 flag 变量的值
 // 而后将 flag 变量设置为 true
 // 因为两个 goroutine 会同时唤醒并尝试获取锁资源,它们会互相期待
 // 最终导致了活锁问题,它们都无奈向前推动
 fmt.Println("主线程休眠 1 秒")
 fmt.Println("两个 goroutine 都应该期待标记")
 flag = true
 wg.Wait()

 fmt.Println("所有 GoRoutines 已实现")
}

革新后的代码在期待 flag 变量的循环中退出了让出工夫片的函数 runtime.Gosched(),这样两个 goroutine 在期待期间能够放弃工夫片,以便其余 goroutine 能够执行并取得锁资源。这种形式能够无效地缩小竞争水平,从而防止了活锁问题。

  1. 如何防止发生存锁的可能性

在 Go 语言的并发编程中,防止活锁的要害是正确地实现同步机制。以下是一些防止活锁的办法:

  • 防止忙期待:应用 sync.Cond 或者 channel 等同步机制来实现期待。这样防止了线程始终占用 CPU 资源而无奈获得停顿的问题。
  • 防止死锁:死锁往往是活锁的前提,因而正确地应用锁和同步机制能够防止死锁,从而防止活锁。
  • 缩小锁的粒度:尽可能将锁的粒度放大到最小范畴,防止锁住不必要的代码块。
  • 采纳超时机制:应用 sync.Mutex 的 TryLock() 办法或者应用 select 语句实现期待超时机制,这样能够避免线程无限期期待。
  • 正当设计并发模型:正当设计并发模型能够防止竞争和饥饿等问题,进而防止活锁的产生。

本文转载于 WX 公众号:不背锅运维(喜爱的盆友关注咱们):https://mp.weixin.qq.com/s/gylcUAOWUkoB7zlve1mAoA

正文完
 0