我在极客时间开了一门面向中高级Go程序员的课程:Go并发编程实战课。有读者问在Gochannel的实现中使用了mutex。这个mutex和标准库中的Mutex有什么区别?正好百度大厂的同事分享了Go相关的课程也提出了同样的问题,所以特地写了一篇介绍一下。sync.Mutex是高级同步原语。它是提供给广大Go开发者开发应用的数据结构。现在它的内部实现逻辑比较复杂,包括自旋和饥饿处理逻辑。它在运行时使用一些低级函数和一些原子方法。运行时互斥量是为运行时内部使用互斥锁提供的同步原语。它提供了自旋和等待队列,但没有解决饥饿状态,其实现与sync.Mutex不同。相同。它不提供Lock/Unlock作为方法,而是提供lock/unlock函数来请求和释放锁。DanScales在今年年初为运行时锁添加了静态锁rank的功能。他定义了runtimearchitecture-independentlocks的rank,定义了一些runtimelocks的partialorder(在这个锁之前允许持有哪些锁)。这是运行时锁的一个巨大变化,可惜没有设计文档详细描述这个功能的设计。您可以通过提交的评论(#0a820007)和代码中的评论了解运行时内部锁的代码变化。本质上,这个函数是用来检查加锁的顺序是否按照文档设计的顺序执行的。如果违反设定的顺序,可能会出现死锁。由于缺乏准确的文档,而且这个函数主要是用来检查运行时锁的执行顺序的,所以我在本文中将这个逻辑抹掉。对于实际的Go运行时启动此检查,您需要设置变量GOEXPERIMENT=staticclockranking。然后再看运行时mutex的数据结构定义和lock/unlock的实现。运行时的互斥量数据结构运行时的互斥量数据结构很简单,如下图,定义在runtime2.go中:}如果不开启lockranking,lockRankStruct其实是一个空结构:typelockRankStructstruct{}那么对于运行时的mutex来说,最重要的就是key域。该字段对于不同的架构有不同的含义。对于dragonfly、freebsd和linux架构,mutex将使用基于Futex的实现,key是一个uint32值。Linux提供的Futex(Fastuser-spacemutexes)用于在用户空间建立锁和信号量。Go运行时封装了两个方法来休眠和唤醒当前线程:futexsleep(addruint32,valuint32,nsint64):原子操作`ifaddr==val{sleep}`。futexwakeup(addr*uint32,cntuint32):唤醒地址addr上的线程最多cnt次。对于其他架构,如aix、darwin、netbsd、openbsd、plan9、solaris、windows,mutex会使用sema-based实现,关键是M*waitm。Go运行时封装了三个创建信号量和睡眠/唤醒的方法:funcsemacreate(mp*m):创建一个信号量funcsemasleep(nsint64)int32:请求一个信号量,如果没有收到请求,会休眠一段时间funcsemawakeup(mp*m):唤醒mp就是基于这两个实现。lock和unlock方法分别有不同的实现。主要逻辑类似,接下来我们只看基于Futex的加锁/解锁。如果请求锁lock不使用锁排序特性,锁的逻辑主要由lock2实现。funclock(l*mutex){lockWithRank(l,getLockRank(l))}funclockWithRank(l*mutex,ranklockRank){lock2(l)}funclock2(l*mutex){//获取g对象gp:=getg()//g绑定的m对象的锁计数加上1ifgp.m.locks<0{throw("runtime·lock:lockcount")}gp.m.locks++//如果有幸运光环,则不持有原锁,a得到锁,快速返回v:=atomic.Xchg(key32(&l.key),mutex_locked)ifv==mutex_unlocked{return}//否则原来可能是MUTEX_LOCKED或者MUTEX_SLEEPINGwait:=v//单核if不执行自旋,多核CPU的情况下会尝试自旋:=0ifncpu>1{spin=active_spin}for{//尝试自旋,如果锁已经释放,则尝试抢锁fori:=0;i
