理解了同步的本质,我们知道可以通过锁,来保护临界区 *** 作的互斥性。如果尝试获得锁失败了,第一种策略是屡败屡战,不断重复尝试,直到成功获得锁或时间片耗完,这被称为自旋锁。第二种策略是让出CPU,进到等待队列里去,我们称之为调度器对象,通俗理解就是 *** 作系统提供的线程间同步原语,一般以一组系统调用的形式存在,基于这些同步原语,可以实现锁以及更复杂的同步工具。
这些调度器对象与自旋锁的不同之处,主要在于等待队列,这些同步原语是由内核提供的,直接与系统的调度器交互,能够挂起和唤醒线程,这一点是自旋锁做不到的。但也正由于其在内核中实现,所以应用程序需要以系统调用的方式来使用它,这就造成了一定的开销,而且获取锁失败时还会发生线程切换,使得开销进一步增大。所以说“调度器对象”和"自旋锁"各有各的应用场景。
如果是多核环境,且持有锁的时间占比比较小的情况往往在几次自旋之后就能拿到锁,这可比发生一次线程切换的代价要小得多。然而若是单核环境,或者持有锁的时间占比较大的情况,一味自旋空耗CPU反而得不偿失,而实际的业务逻辑中,持有锁的时间往往不是很确定,如果加锁时先经过自旋锁,但是限制最大自旋次数,若在有限次数内不能加锁成功,再通过调度器对象将当前线程挂起,这样就结合了二者的优点,也是如今主流的锁的实现思路。
Go语言中的runtime.mutex就是这样的思路(不是sync.mutex)
,它被runtime自身的代码使用,本质上就是结合了自旋锁和调度器对象的优化过的锁。不过它是针对线程设计的,若是协程等待锁时还需要切换线程,那就说不过去了。
那么协程要等待一个锁时,要如何休眠,等待,和唤醒呢?这就要靠runtime.semaphore来实现,这是可供协程使用的信号量。
runtime内部会通过一个大小为251的semtable,来管理所有的semaphore。怎么通过这个大小固定的table,来管理执行阶段数量不定的semaphore呢?
大致思路是这样的,这个sematable存储的是251颗平衡树(AVL)的根,平衡树中每个节点都是一个sudog类型的对象,要使用一个信号量时 ,需要提供一个记录信号量数值的变量, 根据它的地址进行计算, 映射到sematable中的一颗平衡树上,找到对应的节点,就找到了该信号量的等待队列了。例如,我们常用的sync.Mutex中,有一个sema字段,用于记录信号量的数值,如果有协程想要等待这个Mutex,就会根据sema字段的地址计算映射到sematable中的某个平衡树上,找到对应的节点,也就找到了这个Mutex的等待队列,所以sync.Mutex是通过信号量来实现排队的。
channel自己实现队列而channel需要有读等待队列和写等待队列 ,还要支持缓冲区功能,所以并没有直接使用信号量来实现排队,而是自己实现了一套排队逻辑。不过无论是信号量还是channel,底层实现都离不开runtime.mutex,因为它们都需要保障在面临多线程并发时,不会出现同步问题。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)