理理并发:死锁,活锁与饥饿

一、并发问题的产生

若多个cpu需要对同一内存地址中的数据,进行增删改查,必须按先来后到的顺序进行。保证不了顺序,程序就会出现问题。这就是所谓的竞争条件 race condition

二、并发问题的场景

先跑个程序看看:

func main() {
   	runtime.GOMAXPROCS(1) // P的值
	var data = 0
	
	access := func(i int) {
		data++
		fmt.Printf("goroutine:%d  data: %v\n", i, data)
	}

	for i := 0; i < 5; i++ {
		go access(i)
	}
	<-time.After(2 * time.Second)
}

P设置为1,所有goroutine都在一个P的队列中,依次执行。
结果:

	goroutine:0  data: 1
	goroutine:1  data: 2
	goroutine:2  data: 3
	goroutine:3  data: 4
	goroutine:4  data: 5

P设置为5,5个M与之对应,最多5个goroutine可并发的执行,data的值成不可确定的了。
结果:

	goroutine:1  data: 2 //第一次
	goroutine:0  data: 1
	goroutine:3  data: 4
	goroutine:4  data: 5
	goroutine:2  data: 5
	
	goroutine:1  data: 1 // 第二次
	goroutine:0  data: 2
	goroutine:3  data: 4
	goroutine:4  data: 5
	goroutine:2  data: 5

程序中像这种,需要独占的访问共享资源的代码,有一个专门的名词:临界区(critical section)。
为了使共享数据按人规定好的逻辑来进行正常变化,因此需要在程序操作数据时,使其顺序化,先到先得。这个过程也有个说法,叫保护临界区
修改代码:

func main() {
	runtime.GOMAXPROCS(5)
	var data = 0
	var memAccess sync.Mutex
	access := func(i int) {
		memAccess.Lock()
		data++
		fmt.Printf("goroutine:%d  data: %v\n", i, data)
		memAccess.Unlock()
	}
	for i := 0; i < 5; i++ {
		go access(i)
	}
	<-time.After(2 * time.Second)
}

结果:

	goroutine:1  data: 1 //第一次
	goroutine:0  data: 2
	goroutine:3  data: 3
	goroutine:4  data: 4
	goroutine:2  data: 5

	goroutine:0  data: 1 //第二次
	goroutine:1  data: 2
	goroutine:3  data: 3
	goroutine:4  data: 4
	goroutine:2  data: 5

通过对data加互斥锁,保护了临界区,使逻辑有序。
没加锁,程序存在正确性问题。加了锁没加对,程序可能会出现下列三种情况:

  • 死锁
type value struct {
   memAccess sync.Mutex
   value     int
}

func main() {
   runtime.GOMAXPROCS(3)
   var wg sync.WaitGroup
   sum := func(v1, v2 *value) {
   	defer wg.Done()
   	v1.memAccess.Lock()
   	time.Sleep(2 * time.Second)
   	v2.memAccess.Lock()
   	fmt.Printf("sum = %d\n", v1.value+v2.value)
   	v2.memAccess.Unlock()
   	v1.memAccess.Unlock()
   }

   product := func(v1, v2 *value) {
   	defer wg.Done()
   	v2.memAccess.Lock()
   	time.Sleep(2 * time.Second)
   	v1.memAccess.Lock()
   	fmt.Printf("product = %d\n", v1.value*v2.value)
   	v1.memAccess.Unlock()
   	v2.memAccess.Unlock()
   }

   var v1, v2 value
   v1.value = 1
   v2.value = 1
   wg.Add(2)
   go sum(&v1, &v2)
   go product(&v1, &v2)
   wg.Wait()
}

结果:

fatal error: all goroutines are asleep - deadlock!
goroutine1 a.lock b.lock goroutine2 获取a锁 获取b锁 获取a锁 获取b锁 goroutine1 a.lock b.lock goroutine2
  • 活锁:举个例子,路上一个人向你走来,你想移到一边让他先过,结果他也做了同样的动作。然后这种情况一直持续下去,这就是活锁。
func main() {
   runtime.GOMAXPROCS(3)
   cv := sync.NewCond(&sync.Mutex{})
   go func() {
   	for range time.Tick(1 * time.Second) { // 通过tick控制两个人的步调
   		cv.Broadcast()
   	}
   }()
   
   takeStep := func() {
   	cv.L.Lock()
   	cv.Wait()
   	cv.L.Unlock()
   }
   
   tryDir := func(dirName string, dir *int32, out *bytes.Buffer) bool {
   	fmt.Fprintf(out, " %+v", dirName)
   	atomic.AddInt32(dir, 1)
   	takeStep()                      //走上一步
   	if atomic.LoadInt32(dir) == 1 { //走成功就返回
   		fmt.Fprint(out, ". Success!")
   		return true
   	}
   	takeStep() // 没走成功,再走回来
   	atomic.AddInt32(dir, -1)
   	return false
   }
   
   var left, right int32
   tryLeft := func(out *bytes.Buffer) bool {
   	return tryDir("向左走", &left, out)
   }
   
   tryRight := func(out *bytes.Buffer) bool {
   	return tryDir("向右走", &right, out)
   }
   
   walk := func(walking *sync.WaitGroup, name string) {
   	var out bytes.Buffer
   	defer walking.Done()
   	defer func() { fmt.Println(out.String()) }()
   	fmt.Fprintf(&out, "%v is trying to scoot:", name)

   	for i := 0; i < 5; i++ {
   		if tryLeft(&out) || tryRight(&out) {
   			return
   		}
   	}
   	fmt.Fprintf(&out, "\n%v is tried!", name)
   }
   
   var trail sync.WaitGroup
   trail.Add(2)
   go walk(&trail, "男人") // 男人在路上走
   go walk(&trail, "女人") // 女人在路上走
   trail.Wait()
}

结果:

女人 is trying to scoot: 向左走 向右走 向左走 向右走 向左走 向右走 向左走 向右走 向左走 向右走
女人 is tried!
男人 is trying to scoot: 向左走 向右走 向左走 向右走 向左走 向右走 向左走 向右走 向左走 向右走
男人 is tried!
  • 饥饿:贪婪的进程一直占有资源,阻止其他进程甚至全部进程执行任务
func main() {
   runtime.GOMAXPROCS(3)

   var wg sync.WaitGroup
   const runtime = 1 * time.Second
   var sharedLock sync.Mutex

   greedyWorker := func() {
   	defer wg.Done()
   	var count int
   	for begin := time.Now(); time.Since(begin) <= runtime; {
   		sharedLock.Lock()
   		time.Sleep(3 * time.Nanosecond)
   		sharedLock.Unlock()
   		count++
   	}

   	fmt.Printf("Greedy worker was able to execute %v work loops\n", count)
   }

   politeWorker := func() {
   	defer wg.Done()
   	var count int
   	for begin := time.Now(); time.Since(begin) <= runtime; {
   		sharedLock.Lock()
   		time.Sleep(1 * time.Nanosecond)
   		sharedLock.Unlock()

   		sharedLock.Lock()
   		time.Sleep(1 * time.Nanosecond)
   		sharedLock.Unlock()

   		sharedLock.Lock()
   		time.Sleep(1 * time.Nanosecond)
   		sharedLock.Unlock()
   		count++
   	}
   	fmt.Printf("Polite worker was able to execute %v work loops\n", count)
   }

   wg.Add(2)
   go greedyWorker()
   go politeWorker()

   wg.Wait()
}

结果:

Greedy worker was able to execute 273 work loops
Polite worker was able to execute 92 work loops

三、并发问题的总结

锁不用肯定会出问题。用了,解了前面的问题,又出现了更多的新问题。
死锁,是因为错误的使用了锁,导致异常。
活锁,是饥饿的一种特殊情况,逻辑上感觉对,程序也一直在正常的跑,但就是效率低,逻辑上进行不下去。
饥饿,与锁使用的粒度有关,通过计数取样,可以判断进程的工作效率

只要有共享资源的访问,必定要使其逻辑上进行顺序化和原子化,确保访问一致。这绕不开锁这个概念。用锁增加了程序正确性的风险和开发人员脑力上的负担,特别是在一些大型复杂的程序中。

因此,当前解并发问题主流的思路有两种:actor模型与csp模型。他们都是从编程语言的角度去抽象并发模型:不要共享内存,各逻辑块之间,通过通讯解共享内存的问题。

actor模型,强调每个actor有自己的状态和行为。要改变状态,只有通过事件消息,由每个actor进行改变。actor通过邮箱接受外部事件。代表语言有erlang,内部使用了大量的无锁队列,且其语言层面的变量不可重复赋值等特性。每个actor都是一个最基本的计算单元,进入邮箱的事件是保持顺序的,在并发场景下,需要访问共享资源时,进入邮箱时,就保证了其顺序性,actor的调度交给语言层面去做,即这个计算单元什么时候执行,什么时候不执行。

csp模型,本质上来说和actor模型是异曲同工,通过通信达到内存共享的目标,其代表语言golang,通过channel实现不同计算单元(goroutine)间的通讯顺序问题。对共享资源的访问,使用channel和goroutine保证资源的顺序访问,goroutine的调度交给语言层面去做。golang也提供了锁的原语。

参考:《Concurrency in Go》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值