zoukankan      html  css  js  c++  java
  • [.net 多线程]SpinWait

    《CLR via C#》读书笔记-线程同步(四)

    混合线程同步构造简介

    之前有用户模式构造和内核模式构造,前者快速,但耗费CPU;后者可以阻塞线程,但耗时、耗资源。因此.NET会有一些混合了两者的构造,《CLR via C#》的作者给这些构造起了一个别名:混合线程同步构造(Hybrid Thread Synchronization Construct)


    混合线程同步构造的例子

    混合线程同步构造的例子如下:

      internal sealed class SimpleHybridLock : IDisposable
        {
            private int m_waiters = 0;
            private AutoResetEvent m_waiterLock = new AutoResetEvent(false);
    
            public void Enter()
            {
                if (Interlocked.Increment(ref m_waiters) == 1)
                    return;
    
                m_waiterLock.WaitOne();
            }
    
            public void Leave()
            {
                if (Interlocked.Decrement(ref m_waiters) == 0)
                    return;
    
                m_waiterLock.Set();
            }
    
            public void Dispose() { m_waiterLock.Dispose(); }
        }

    例子很简单,初次使用用户模式判断;若有线程竞争者,则使用内核模式的进行线程阻塞。 
    在混合线程同步构造中有四个性能考虑点:内核对象的创建、Dispose、Enter方法、Leave方法。其中可主要考虑Enter及Leave方法。但是在.NET中其也提供了AutoResetEventSlim构造,其使用了“延迟加载”方法,即,只有当内核对象初次使用时(即第一次检测到竞争时),才会创建AutoResetEvent,这样可以避免性能损失。 
    上面的例子中,任何线程都可以调用Leave方法。所以这方法不够严谨。因此可以在Enter及Leave方法中添加相关用于记录获取同步锁的线程信息的字段,这样就能保证做到只有获得同步锁的线程才能调用Leave方法。下面的例子进行了说明:

        internal sealed class AnotherHybridLock : IDisposable
        {
    
            private int m_waiters = 0;
            private AutoResetEvent m_waiterLock = new AutoResetEvent(false);
            private int m_spincount = 4000;
            private int m_owningTheadID = 0, m_recursion = 0;
    
            public void Enter()
            {
                //若相同的线程调用Enter方法,则增加一次循环记录后,返回
                int threadID = Thread.CurrentThread.ManagedThreadId;
                if (threadID == m_owningTheadID)
                {
                    m_recursion++;
                    return;
                }
    
                //若第一个线程使用通过内核模式获得同步锁后,紧随其后的第二个线程并不会立刻调用内核模式
                //而是通过一个循环,碰碰运气,看能否在循环内得到同步锁
                SpinWait spinWait = new SpinWait();
                for (int spinCount = 0; spinCount < m_spincount;spinCount++ )
                {
                    if (Interlocked.CompareExchange(ref m_waiters, 1, 0) == 0) goto GotLock;
    
                    spinWait.SpinOnce();
                }
    
                //若还是没有得到,只能调用内核模式,等待获取同步锁
                if (Interlocked.Increment(ref m_waiters)>1)
                {
                    m_waiterLock.WaitOne();
                }
    
    
            GotLock:
                m_owningTheadID = threadID; m_recursion = 1;
            }
    
            public void Leave()
            {
                int threadID = Thread.CurrentThread.ManagedThreadId;
                if (threadID!=m_owningTheadID)
                {
                    throw new SynchronizationLockException("Leave被非原线程调用!");
                }
    
                //代表同一线程多次调用Leave方法,则进行--m_recursion后,直接返回
                if (--m_recursion > 0) return;
    
                //代表调用Leave的线程目前只有一次Enter,因此调用Leave方法释放同步锁
                //将当前的线程ID置位0
                m_owningTheadID = 0;
    
                //代表外界无等待的线程,则直接返回
                if (Interlocked.Decrement(ref m_waiters) == 0) return;
    
                //代表外界存在等在同步锁的线程,则通过内核方法,释放同步锁
                //使等待线程获取同步锁,解除阻塞
                m_waiterLock.WaitOne();
            }
    
            public void Dispose() { m_waiterLock.Dispose(); }
        }

    在上面的两个例子中:有一个特点:用户模式只能提供一个同步锁,若还有多线程同时访问锁,则使用内核模式。这样才能发挥用户模式的“快”和内核模式的“省”。另外,第二个例子(AnotherHybridLock)与第一个相比:1、对象占用内存要大;2、Enter与Leave的性能要低。鱼与熊掌不可兼得嘛!

    SpinWait结构

    在后一个例子中有这样一段代码:

    SpinWait spinWait = new SpinWait();
    for (int spinCount = 0; spinCount < m_spincount;spinCount++ )
    {
        if (Interlocked.CompareExchange(ref m_waiters, 1, 0) == 0) goto GotLock;
        spinWait.SpinOnce();
    }

    这里面有一个SpinWait结构(其实还有一个Thread.SpinWait方法),对这个结构比较感兴趣,所以就去看了看MSDN的解释。 
    SpinWait结构是一种可以在小脚本(low-level scenarios)中使用,并且可以避免上下文切换和内核转换的轻量级类型。说白了,其是一种“智能”的自旋方式(这里的智能是指,内部添加了一些算法,使自旋不仅仅是简单的自旋,还有一些其他的功能,帮助提升性能)。另外,SpinWait在自旋一段时间后,也会让出CPU(并不是一直在自旋),这样CPU可以处理其他的线程,而不是傻傻的一直等待自旋结束。 
    SpinWait结构的属性和方法如下: 
    这里写图片描述 
    这里写图片描述 
    这么多属性和方法中只有两个最常用,一个是属性:NextSpinWillYield;一个是方法:SpinOnce() 
    SpinOnce()方法:方法内部有一个if…else…判断。如果NextSpinWillYield返回true,则方法内部通过调用Thread.Sleep(0)、Thread.Sleep(1)、Thread.Yield()方法让出CPU,否则调用Thread.Spinwait()方法使其继续自旋。 
    NextSpinWillYield:其决定了调用SpinOnce方法的线程是否应该让出CPU。若返回true,则调用SpinOnce()方法的线程会让出CPU;返回false,则线程仍将自旋。 
    下面通过一个例子说明:

       class Program
        {
            static void Main(string[] args)
            {
                bool someBoolean = false;
                int numYields = 0;
    
                //线程1
                Task t1 = Task.Factory.StartNew(() =>
                {
                    SpinWait sw = new SpinWait();
                    while (!someBoolean)
                    {
                        //NextSpinWillYield属性返回true,则调用SpinOnce方法的线程会让出CPU
                        //否则,自旋
                        if (sw.NextSpinWillYield) numYields++;
                        sw.SpinOnce();
                    }
    
                    Console.WriteLine("SpinWait called {0} times, yielded {1} times", sw.Count, numYields);
                });
    
                //第二个任务,在0.1秒后将someBoolean置为true
                Task t2 = Task.Factory.StartNew(() =>
                {
                    Thread.Sleep(100);
                    someBoolean = true;
                });
    
                //等待两个任务完成
                Task.WaitAll(t1, t2);
                Console.ReadLine();
            }
        }

    SpinWait结构源码

    下面通过SpinWait结构的源代码进行说明 
    这里写图片描述 
    这里面有一个内部变量m_count,其用来记录SpinOnce方法的调用次数。 
    先看一下属性NextSpinWillYield的源代码:

    public bool NextSpinWillYield
    {
        get
        {
            if (this.m_count <= 10)
            {
                return PlatformHelper.IsSingleProcessor;
            }
            return true;
        }
    }

    哈哈哈,其逻辑就是:若调用SpinOnce方法10次以内,看看电脑是否为单核电脑。否则就返回true。 
    再看看SpinOnce()的源代码:

    public void SpinOnce()
    {
        //在方法内部,其还是调用一次NextSpinWillYield属性,根据属性的结果,决定是让出CPU还是自旋
    
        if (this.NextSpinWillYield)  //为true,则代表让出CPU。只不过,需要根据m_count的值决定使用何种方法
        {
            CdsSyncEtwBCLProvider.Log.SpinWait_NextSpinWillYield();
            int num = (this.m_count >= 10) ? (this.m_count - 10) : this.m_count;
            if ((num % 20) == 0x13)
            {
                Thread.Sleep(1);
            }
            else if ((num % 5) == 4)
            {
                Thread.Sleep(0);
            }
            else
            {
                Thread.Yield();
            }
        }
        else
        {
            //让CPU进行时间为:this.m_count*16的自旋
            Thread.SpinWait(((int) 4) << this.m_count);
        }
        //0x7fffffff,其为int32的最大值。即,若m_count到了最大值后,从10开始。这样NextSpinWillYield将会一直返回true
        this.m_count = (this.m_count == 0x7fffffff) ? 10 : (this.m_count + 1);
    }

    以上就是SpinWait的源代码内容。 
    另外,在SpinWait结构内容使用了Thread.Sleep(1)、Thread.Sleep(0)、Thread.Yield()方法,这三者方法具体的差别可参见一篇博客:三个方法的具体区别

  • 相关阅读:
    UVALive 7141 BombX
    CodeForces 722D Generating Sets
    CodeForces 722C Destroying Array
    CodeForces 721D Maxim and Array
    CodeForces 721C Journey
    CodeForces 415D Mashmokh and ACM
    CodeForces 718C Sasha and Array
    CodeForces 635C XOR Equation
    CodeForces 631D Messenger
    田忌赛马问题
  • 原文地址:https://www.cnblogs.com/deepminer/p/9064040.html
Copyright © 2011-2022 走看看