loong博客

死锁与活锁?

Deadlock

  1. 死锁产生的必要条件
  • Mutual Exclusion (互斥条件)

    资源一旦分配给某个进程,该进程在某一段时间内拥有对这个资源的专有权,直到该进程释放这个资源。

  • Wait For Condition (请求与保持)

    进程已经占有了某个资源,同时又在请求新的资源,但对已占有的资源不释放。

  • No Preemption (不可抢占)

    资源只能被占有它的进程释放,其它进程不能抢占。

  • Circular Wait (循环等待)

    若干进程形成了首尾相连的循环等待资源关系。

  1. 死锁示例

package main

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

type value struct {
	mu    sync.Mutex
	value int
}

func main() {
	var wg sync.WaitGroup
	printSum := func(v1, v2 *value) {
		defer wg.Done()
		v1.mu.Lock()
		defer v1.mu.Unlock()
		time.Sleep(2 * time.Second)
		v2.mu.Lock()
		defer v2.mu.Unlock()
		fmt.Printf("sum=%v\n", v1.value+v2.value)
	}
	var a, b value
	wg.Add(2)
	go printSum(&a, &b)
	go printSum(&b, &a)
	wg.Wait()
}

3.为什么发生死锁?

死锁图解

在上面的示例中,我们创建了2个goroutine。一个goroutine执行printSum时,锁住了a, 同时试图再去锁b。另一个goroutine执行printSum时,锁住了b, 同时试图再去锁a。两个goroutine都在等待对方释放资源,所以发生了死锁。

Livelock

  1. 什么是活锁?

活锁是指正在执行的线程或进程没有发生阻塞,由于某些条件没有满足,导致反复重试-失败-重试-失败的过程。与死锁最大的区别在于活锁状态的线程或进程是一直处于运行状态的,在失败中不断重试,重试中不断失败,一直处于所谓的“活动”状态,不会停止。

  1. 活锁示例

package main

import (
	"fmt"
	"log"
	"sync"
	"sync/atomic"
	"time"
)

var (
	// 定义互斥信号量来同步
	// 创建实例
	cond = sync.NewCond(&sync.Mutex{})

	// 两个人会面时,为了给对方让路,会向左或向右移动

	// 向左移动计数器
	leftCount int32

	// 向右移动计数器
	rightCount int32
)

func takeStep() {
	cond.L.Lock()
	// 等待 cond 被 Broadcast 或 Signal 唤醒
	cond.Wait()
	cond.L.Unlock()
}

// 移动
func move(name string, dir string, count *int32) bool {
	fmt.Printf("%s 走到了 %v\n", name, dir)

	// 当前方向计数器加1
	atomic.AddInt32(count, 1)

	takeStep()

	// 如果当前计数器被人修改过, 说明这个人移动了方向,此时可以让对方先走,程序直接返回
	if atomic.LoadInt32(count) == 1 {
		// 因为活锁,所以该代码永远不执行
		fmt.Printf("%s 给对方让路成功 \n", name)
		return true
	}

	// 没有让路成功,再走回原位置
	takeStep()

	// 当前方向计数器减1
    atomic.AddInt32(count, -1)

	return false
}

func giveWay(name string) {
	fmt.Printf("%s 尝试给对方让路 ... \n", name)
	// 模拟三次双方互相让路
	for i := 0; i < 3; i++ {
		if move(name, "左边", &leftCount) || move(name, "右边", &rightCount) {
			return
		}
	}
	fmt.Printf("%v 无奈地说: 咱们可以停止互相给对方让路了,你先过!\n", name)
}

func main() {
	go func() {
		// 1 毫秒之后发出通知,释放锁
		for range time.Tick(1 * time.Millisecond) {
			log.Println("broadcast", time.Now().UnixMilli())
			cond.Broadcast()
		}
	}()

	var wg sync.WaitGroup

	wg.Add(2)

	go func() {
		defer wg.Done()
		giveWay("小张")
	}()

	go func() {
		defer wg.Done()
		giveWay("小王")
	}()

	wg.Wait()
}


输出结果如下:


小王 尝试给对方让路 ... 
小王 走到了 左边
小张 尝试给对方让路 ... 
小张 走到了 左边
2024/04/28 08:48:49 broadcast 1714265329258
2024/04/28 08:48:49 broadcast 1714265329259
小王 走到了 右边
小张 走到了 右边
2024/04/28 08:48:49 broadcast 1714265329260
2024/04/28 08:48:49 broadcast 1714265329261
小王 走到了 左边
小张 走到了 左边
2024/04/28 08:48:49 broadcast 1714265329262
2024/04/28 08:48:49 broadcast 1714265329263
小王 走到了 右边
小张 走到了 右边
2024/04/28 08:48:49 broadcast 1714265329264
2024/04/28 08:48:49 broadcast 1714265329266
小王 走到了 左边
小张 走到了 左边
2024/04/28 08:48:49 broadcast 1714265329267
2024/04/28 08:48:49 broadcast 1714265329268
小王 走到了 右边
小张 走到了 右边
2024/04/28 08:48:49 broadcast 1714265329269
2024/04/28 08:48:49 broadcast 1714265329270
小王 无奈地说: 咱们可以停止互相给对方让路了,你先过!
小张 无奈地说: 咱们可以停止互相给对方让路了,你先过!

3.为什么发生活锁?

在上面的示例中,两个或多个并发进程试图在不协调的情况下防止死锁。如果走廊上的人彼此同意只有一个人会移动,那么就不会出现活锁了。