2

Go运行时中的 Mutex

 3 years ago
source link: https://colobu.com/2020/12/06/mutex-in-go-runtime/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我在极客时间上开了一门面向中高级Go程序员的课程: Go 并发编程实战课 ,有读者问Go channel中的实现中使用了mutex,这个mutex和标准库中的Mutex有什么不同?正好在百度厂内分享Go相关课程中有同事也提出了相同的问题,所以我专门写一篇文章介绍一下。

sync.Mutex 是一个high level的同步原语,是为广大的Go开发者开发应用程序提供的一种数据结构,现在它的内部实现逻辑比较复杂了,包含spin和饥饿处理等逻辑,它底层使用了运行时的low level的一些函数和atomic的一些方法。

而运行时中的mutex是为运行时内部使用互斥锁而提供的一个同步原语,它提供了spin和等待队列,并没有去解决饥饿状态,而且它的实现和sync.Mutex的实现也是不一样的。它并没有以方法的方式提供Lock/Unlock,而是提供lock/unlock函数实现请求锁和释放锁。

Dan Scales 今年年初的时候又为运行时的锁增加了static locking rank的功能。他为运行时的架构无关的锁( architecture-independent locks)定义了rank,并且又定义了一些运行时的锁的偏序(此锁之前允许持有哪些锁)。这是运行时锁的一个巨大改变,但是很遗憾并没有一篇设计文档详细去描述这个功能的设计,你可以通过提交的comment( #0a820007 )和代码中的 注释 去了解runtime内部锁的代码变化。

本质上来说,这个功能用来检查锁的顺序是不是按照文档设计的顺序执行的,如果有违反设定的顺序,就有可能死锁发生。因为缺乏准确的文档说明,并且这个功能主要是用来检查运行时锁的执行顺序的,所以在本文中我把这一段逻辑抹去不介绍了。实际Go运行时要开始这个检查的话,你需要设置变量 GOEXPERIMENT=staticlockranking

那么接下来我们看看运行时的mutex的数据结构的定义以及lock/unlock的实现。

运行时mutex数据结构

运行时的mutex数据结构很简单,如下所示,定义在 runtime2.go 中:

type mutex struct {
    lockRankStruct
    // Futex-based impl treats it as uint32 key,
	// while sema-based impl as M* waitm.
	// Used to be a union, but unions break precise GC.
	key uintptr
}

如果不启用lock ranking,其实lockRankStruct就是一个空结构:

type lockRankStruct struct {
}

那么对于运行时的mutex,最重要的就是key字段了。这个字段针对不同的架构有不同的含义。

对于 dragonflyfreebsdlinux 架构,mutex会使用 基于Futex的实现 , key就是一个uint32的值。 Linux提供的Futex(Fast user-space mutexes)用来构建用户空间的锁和信号量。Go 运行时封装了两个方法,用来sleep和唤醒当前线程:

  • futexsleep(addr uint32, val uint32, ns int64):原子操作`if addr == val { sleep }`。
  • futexwakeup(addr *uint32, cnt uint32):唤醒地址addr上的线程最多cnt次。

对于其他的架构,比如 aixdarwinnetbsdopenbsdplan9solariswindows ,mutex会使用 基于sema的实现 ,key就是 M* waitm 。Go 运行时封装了三个方法,用来创建信号量和sleep/wakeup:

  • func semacreate(mp *m):创建信号量
  • func semasleep(ns int64) int32: 请求信号量,请求不到会休眠一段时间
  • func semawakeup(mp *m):唤醒mp

基于这两种实现,分别有不同的lock和unlock方法的实现,主要逻辑都是类似的,所以接下来我们只看基于Futex的lock/unlock。

请求锁lock

如果不使用lock ranking特性,lock的逻辑主要是由lock2实现的。

func lock(l *mutex) {
	lockWithRank(l, getLockRank(l))
}

func lockWithRank(l *mutex, rank lockRank) {
	lock2(l)
}

func lock2(l *mutex) {
    // 得到g对象
	gp := getg()

    // g绑定的m对象的lock计数加1
	if gp.m.locks <0 {
		throw("runtime·lock: lock count")
	}
	gp.m.locks++

	// 如果有幸运光环,原来锁没有被持有,一把就获取到了锁,就快速返回了
	v := atomic.Xchg(key32(&l.key), mutex_locked)
	if v == mutex_unlocked {
		return
	}

    // 否则原来的可能是MUTEX_LOCKED或者MUTEX_SLEEPING
	wait := v

    // 单核不进行spin,多核CPU情况下会尝试spin
	spin :=0
	if ncpu >1 {
		spin = active_spin
    }
    

	for {
		// 尝试spin,如果锁已经释放,尝试抢锁
		for i :=0; i < spin; i++ {
			for l.key == mutex_unlocked {
				if atomic.Cas(key32(&l.key), mutex_unlocked, wait) {
					return
				}
            }
            // PAUSE
			procyield(active_spin_cnt)
		}

		// 再尝试抢锁, rescheduling.
		for i :=0; i < passive_spin; i++ {
			for l.key == mutex_unlocked {
				if atomic.Cas(key32(&l.key), mutex_unlocked, wait) {
					return
				}
			}
			osyield()
		}

		// 再尝试抢锁,并把key设置为mutex_sleeping,如果抢锁成功,返回
		v = atomic.Xchg(key32(&l.key), mutex_sleeping)
		if v == mutex_unlocked {
			return
        }
        
        // 否则sleep等待
		wait = mutex_sleeping
		futexsleep(key32(&l.key), mutex_sleeping,-1)
	}
}

unlock

如果不使用lock ranking特性,unlock的逻辑主要是由unlock2实现的。

func unlock(l *mutex) {
	unlockWithRank(l)
}

func unlockWithRank(l *mutex) {
	unlock2(l)
}

func unlock2(l *mutex) {
    // 将key的值设置为mutex_unlocked
	v := atomic.Xchg(key32(&l.key), mutex_unlocked)
	if v == mutex_unlocked {
		throw("unlock of unlocked lock")
    }
    // 如果原来有线程在sleep,唤醒它
	if v == mutex_sleeping {
		futexwakeup(key32(&l.key),1)
	}

    //得到当前的goroutine以及和它关联的m,将锁的计数减1
	gp := getg()
	gp.m.locks--
	if gp.m.locks <0 {
		throw("runtime·unlock: lock count")
	}
	if gp.m.locks ==0 && gp.preempt { // restore the preemption request in case we've cleared it in newstack
		gp.stackguard0 = stackPreempt
	}
}

总体来说,运行时的mutex逻辑还不太复杂,主要是需要处理不同的架构的实现,它休眠唤醒的对象是m,而sync.Mutex休眠唤醒的对象是g。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK