golang 系列: mutex 讲解
发布网友
发布时间:2024-09-29 03:28
我来回答
共1个回答
热心网友
时间:2024-10-01 22:22
Go语言设计初衷便是为了高并发场景,因此在资源竞争的场景下,使用mutex的Lock()和Unlock()方法来管理公共资源成为关键。虽然方法调用简单,但其内部机制却较为复杂。让我们深入剖析mutex的运作原理。
mutex的源代码位于src/sync/mutex.go文件,其结构简洁,关键在于信号量标记位sema。信号量机制用于管理goroutine之间的阻塞与唤醒。理解信号量与PV原语操作有助于我们更好地掌握mutex的功能。
PV原语解释了信号量S的用途:当S>0时,表示有S个资源可用;S=0时,表示无资源可用;S<0时,其绝对值表示等待队列中的进程数量。S的初始值应大于等于0。P操作是请求资源,原子性地将S减1,若减1后S仍大于等于0,则继续执行;若减1后S<0,则进程被阻塞并加入等待队列。V操作是释放资源,原子性地将S加1,若加1后S仍大于0,则继续执行;若加1后S≤0,则唤醒等待队列中的进程。
mutex通过信号量实现了goroutine的阻塞与唤醒,其本质上是关于信号量的阻塞唤起操作。当goroutine无法获取锁资源时会被挂起阻塞,无法执行后续代码逻辑。当mutex释放锁资源时,会唤醒之前阻塞的goroutine以抢占锁资源。
mutex的状态标志位由32位组成,其中低3位分别表示唤醒状态、上锁状态和饥饿状态,剩余位表示当前阻塞等待的goroutine数量。mutex根据state状态进入正常模式、饥饿模式或自旋模式。
在正常模式下,当调用Unlock()方法释放锁资源时,如果发现等待唤起的goroutine队列,会唤醒队头的goroutine。队头的goroutine被唤醒后,尝试使用CAS方法修改state状态,成功则表示成功获取锁资源。
饥饿模式发生在队头goroutine无法在一定时间内获取锁资源,且有新来的goroutine尝试获取资源的情况下。在这种情况下,Go会直接将锁资源交给队头goroutine,并将当前状态改为饥饿模式。后面的新来goroutine会直接添加到等待队列的队尾。
当锁资源长时间被占用时,为了减少阻塞唤起goroutine的开销,mutex会让当前的goroutine空转CPU,直到不满足自旋条件,最终加入等待队列。自旋条件需满足特定要求,以避免不必要的CPU消耗。
mutex的Lock()过程如下:如果m.state=0,即没有goroutine占有资源,也不需要阻塞等待唤起的goroutine。尝试使用CAS方法占有锁资源,不执行其他操作。若不满足m.state=0,进一步判断是否需要自旋。如果不需要自旋或自旋后仍无法获取资源,则调用runtime_SemacquireMutex函数阻塞当前goroutine并加入等待队列。当锁资源释放,mutex唤醒队头的goroutine后,队头goroutine尝试占有锁资源,此时可能与其他新来的goroutine竞争。当队头goroutine长时间无法获取资源时,进入饥饿模式,直接将锁资源交给队头goroutine,让新来的goroutine阻塞并加入等待队列的队尾。
Unlock过程相对简单,只需检查是否存在等待唤起的goroutine。在正常模式下,直接唤醒队头goroutine。在饥饿模式下,将锁资源交给队头goroutine,并唤醒它继续运行。
接下来,我们将详细解析mutex的Lock()和Unlock()方法的代码流程,提供更深入的理解。对于感兴趣的朋友,欢迎关注公众号「阅新技术」,获取更多推送文章。感谢大家的支持!