自旋锁的实现原理

自旋锁的实现原理

  1. 自旋锁的介绍
  • 自旋锁和互斥锁比较相似,都是为了实现保护共享资源而提出的一种锁机制,在任何一个时刻,只有一个执行单元可以获取该锁,如果该锁已经被别的单元占用,那么调用者便会进行CPU空转消耗并且时刻关注该所是否已经被释放,直到自己获取该锁为止。
  1. 自旋锁的特点
  • 相对于互斥锁,自旋锁是一种轻量级的锁,在有别的线程获取了该锁,需要进行自旋等待的时候,CPU依然占用着该线程资源不放,不会切换到其他线程去执行,因此,在等待时间较长的时候是不适用自旋锁的,这会拜拜消耗大量的CPU性能。
  • 而互斥锁,在需要进行等待的时候,是不会一直空转消耗CPU的,它会阻塞并且切换到别的线程执行,发生一个上下文切换,这也是一个较为耗时的操作,在重新再切换回该线程执行时,就已经发生了两次上下文切换。
  • 总的来说,在锁的竞争不繁忙,和该锁保持的代码执行时间较短的情况下,是可以使用自旋锁的,这样不会因为等待时间过长而白白浪费了大量的CPU性能。在C#中,SpinWait和SpinLock是基于自旋的等待操作,而Lock、Mutex和各种信号量,都是基于阻塞的等待操作。
  1. C#中的自旋操作
  • 如上所说,SpinWait和SpinLock是C#中的自旋操作,由于自旋锁的自旋操作带来了一定的风险性,比如活锁,CPU一直进行空转等待,所以在C#中的自旋操作是一种自适应的自旋操作,其部分源代码如下:
        internal const int YIELD_THRESHOLD = 10;//作上下文切换操作的阈值
        internal const int SLEEP_0_EVERY_HOW_MANY_TIMES = 5;//每自旋5次sleep(0)一次
        internal const int SLEEP_1_EVERY_HOW_MANY_TIMES = 20;//每自旋20次sleep(1)一次
        private int m_count;//自旋次数
        
        [__DynamicallyInvokable]
        public int Count
        {
            [__DynamicallyInvokable]
            get
            {
                return m_count;
            }
        }
        
        [_DynamicallyInvokable]
        public bool NextSpinWillYield
        {
            [__DynamicallyInvokable]
            get
            {
                if (m_count <= 10)//当自旋次数不超过10次时,单核CPU则返回true,
                {
                    return PlatformHelper.IsSingleProcessor;
                }
                return true;//自旋次数超过10次也将返回true
            }
        }
        
        [__DynamicallyInvokable]
        public void SpinOnce()
        {
            if (NextSpinWillYield)//如果下次操作将产生上下文切换
            {
                CdsSyncEtwBCLProvider.Log.SpinWait_NextSpinWillYield();
                //只有单核CPU才会m_count不大于10,多核CPU从10以后开始进行计数
                int num = (m_count >= 10) ? (m_count - 10) : m_count;
                if (num % 20 == 19)//10以后的计数值除以20后的余数为19则触发一次sleep(1)操作
                {
                    Thread.Sleep(1);
                }
                else if (num % 5 == 4)//10以后的计数值除以5后的余数为4则触发一次sleep(0)操作
                {
                    Thread.Sleep(0);
                }
                else//上述条件都不满足则进行Yield操作
                {
                    Thread.Yield();
                }
            }
            else
            {
                Thread.SpinWait(4 << m_count);//如果不发生上下文切换,这次自旋操作将导致线程等待4*2的m_count的时间
            }
            //当m_count到达int类型计数最大值时重新赋值为10,否则m_count加一
            m_count = ((m_count == 2147483647) ? 10 : (m_count + 1));
        }
        
        [__DynamicallyInvokable]
        public static bool SpinUntil(Func<bool> condition, int millisecondsTimeout)//该方法将导致线程在一定时间内自旋等待条件完成
        {
            if (millisecondsTimeout < -1)//超时时间不能为负
            {
                throw new ArgumentOutOfRangeException("millisecondsTimeout", millisecondsTimeout, Environment.GetResourceString("SpinWait_SpinUntil_TimeoutWrong"));
            }
            if (condition == null)//条件不能为空
            {
                throw new ArgumentNullException("condition", Environment.GetResourceString("SpinWait_SpinUntil_ArgumentNull"));
            }
            uint num = 0u;
            if (millisecondsTimeout != 0 && millisecondsTimeout != -1)
            {
                num = TimeoutHelper.GetTime();//获取当前时间
            }
            SpinWait spinWait = default(SpinWait);
            while (!condition())//条件不满足时,将执行自旋等待,超时时间等于0或者确实超时了将会返回false
            {
                if (millisecondsTimeout == 0)
                {
                    return false;
                }
                spinWait.SpinOnce();//执行自旋操作
                if (millisecondsTimeout != -1 && spinWait.NextSpinWillYield && millisecondsTimeout <= TimeoutHelper.GetTime() - num)
                {
                    return false;
                }
            }
            return true;//在指定超时时间内条件满足则返回false
        }
  • 在SpinWait中,在自旋次数超过10之后,每次进行自旋便会触发上下文切换的操作,在这之后每自旋5次会进行一次sleep(0)操作,每20次会进行一次sleep(1)操作。
  • 通过查看SpinOnce的源码,可以看到,在多核CPU的情况下,在自旋次数10次以内,每次自旋会导致该线程等待4*2的m_count的时间,自旋次数超过10次之后,每20次自旋的第19次会进行sleep(1)一次,每5次自旋的第4次会进行sleep(0)一次,其余都是yield()操作。
  • 对于该线程来说,yield()会导致CPU强制将当前线程切换为当前已经准备好的另外一个线程,即使这个线程的优先级更低,sleep(0)则是将当前线程重新放回该优先级的队列,重新进行一次线程调度,如果没有更高优先级或者相同优先级的就绪线程,CPU可能会再次调回该线程,而sleep(1)会使该线程在未来的1ms的时间内不会成为就绪状态,将不参与当前CPU的竞争。这三种方式都会直接强制该线程放弃剩余的当前的时间片,重新进行线程调度。
  • 再来看SpinUntil的源码,这个方法允许在一个指定的时间内,等待某条件的完成,首先它的指定时间不能为负,条件不能为null,接下来,便使用一个While循环,判断该条件是否满足,每判断一次条件,会判断时间是否已经到达,也会执行一次自旋操作SpinOnce(),若时间到达条件还没能满足则会返回false,在指定时间内满足了则返回true。这也是使用SpinOnce()的一种标准的操作,在C#的其他很多地方中对SpinOnce()方法的使用也是这样的。

你可能感兴趣的:(C#多线程编程)