• [.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()方法,这三者方法具体的差别可参见一篇博客:三个方法的具体区别

  • 相关阅读:
    shmget() 建立共享内存
    [转]SQL2005 连接问题处理
    [转]工作以后十不要,自勉
    C#学习笔记
    一位软件工程师6年总结(转)
    时间相关处理
    Litter Tips
    [转] VS打开解决方案时报错的处理方法
    面向对象—设计模式
    SQL Server 2000中的错误
  • 原文地址:https://www.cnblogs.com/deepminer/p/9064040.html
Copyright © 2020-2023  润新知