【C# 锁】SpinWait结构 -同步基元|同步原语

锁提供了线程安全三要素中的 有序性。二、SpinWait是同步锁的核心,而Thread是SpinWait的核心。

Spinwait结构

完成代码:SpinWait.cs (dot.net)

SpinWait结构的核心代码

internal const int YieldThreshold = 10; // When to switch over to a true yield.
private const int Sleep0EveryHowManyYields = 5; // After how many yields should we Sleep(0)?
internal const int DefaultSleep1Threshold = 20; // After how many yields should we Sleep(1) frequently?

private void SpinOnceCore(int sleep1Threshold)
        {
          if ((
                    _count >= YieldThreshold &&
                    ((_count >= sleep1Threshold && sleep1Threshold >= 0) || (_count - YieldThreshold) % 2 == 0)
                ) ||
                Environment.IsSingleProcessor)
            {
       
 
                if (_count >= sleep1Threshold && sleep1Threshold >= 0)
                {
                    Thread.Sleep(1);
                }
                else
                {
                    int yieldsSoFar = _count >= YieldThreshold ? (_count - YieldThreshold) / 2 : _count;
                    if ((yieldsSoFar % Sleep0EveryHowManyYields) == (Sleep0EveryHowManyYields - 1))
                    {
                        Thread.Sleep(0);
                    }
                    else
                    {
                        Thread.Yield();
                    }
                }
            }
            else
            {
              //线程获取一个每次自旋迭代的最佳最大自旋等待
                int n = Thread.OptimalMaxSpinWaitsPerSpinIteration;
                if (_count <= 30 && (1 << _count) < n)
                {
                    n = 1 << _count;//1向左移动_count位 .
                }
                Thread.SpinWait(n);
            }
 
            // Finally, increment our spin counter.
            _count = (_count == int.MaxValue ? YieldThreshold : _count + 1);
        }

 

原理:SpinWait结构就是对Thread.SpinWait方法的一个简单包装,一个循环次数的策略的。

SpinOnce() 执行次数超过10次之后,每次进行自旋便会触发Thread.Yield()上下文切换的操作,在这之后每5次会进行一次sleep(0)操作,每20次会进行一次sleep(1)操作。SpinOnce()执行一次是大概7个时钟周期。第一自旋例外,第一次的时候比较耗时。

SpinOnce(int32 num) 表示执行num次后进入sleep(1); 


Thread.Sleep(0) 表示让优先级高的线程插队,如果没有线程优先级更高线程插队,那么就继续执行。
Thread.Yield() 让有需要的线程先执行,忽略线程优先级(低级别的线程也可以在core运行),而该线程进入就绪队列。

SpinWait 内部用的是Thread.SpinWait(),Thread.SpinWait()调用外部函数(private static extern void SpinWaitInternal(int iterations);)现实自旋。

 Thread.SpinWait()的注解

Thread.SpinWait本质上是将处理器放入一个非常紧凑的循环中,循环计数由迭代参数指定。因此,等待的时间长短取决于处理器的速度。说白了SpinWait就是一个for循环,我们只要输入一个数字就行。总的循环的时间由处理器的处理速度决定。
SpinWait对于普通应用程序通常不是很有用。在大多数情况下,应该使用.net框架提供的同步类

在极少数情况下,避免使用上下文切换很有用,例如,当你知道状态更改即将发生时,请 SpinWait 在循环中调用方法。 执行的代码 SpinWait 旨在防止具有多个处理器的计算机上出现的问题。 例如,在具有多个采用 Hyper-Threading 技术的 Intel 处理器的计算机上, SpinWait 在某些情况下防止处理器不足。

 

属性

count 表示第几次执行SpinWait

isNextSpinWillYield :thread.SpinWait执行超过10次,开始yield模式
源代码:

    public bool NextSpinWillYield
    {
        get
        {
            if (_count < 10)
            {
                return Environment.IsSingleProcessor;
            }
            return true;
        }
    }

方法

Reset();将count设置未0。

SpinUntil(Func<Boolean>)     在指定条件得到满足之前自旋。内部使用的是 while (!condition()) {spinWait.SpinOnce() 其他代码}
SpinUntil(Func<Boolean>, Int32)     在指定条件得到满足或指定超时过期之前自旋。内部使用的是spinWait.SpinOnce()
SpinUntil(Func<Boolean>, TimeSpan)     在指定条件得到满足或指定超时过期之前自旋。内部使用的是spinWait.SpinOnce()

SpinOnce(int32 num) 中 num 表示执行num次SpinOnce()操作后进入sleep(1);

SpinOnce()\SpinOnce(int32 num):

默认执行20次SpinOnce()操作后进入 sleep(1)。执行SpinOnce()在次数超过10之后,每次进行便会触发Thread.Yield()上下文切换的操作,在这之后每5次会进行一次sleep(0)操作,每20次会进行一次sleep(1)操作。

SpinOnce()和SpinOnce(int32 num)方法前10次调用的是Thread.SpinWait()方法;

前10次 源代码

int n = Thread.OptimalMaxSpinWaitsPerSpinIteration;//大概10以内,可以通过vs2022 调试源代码查看该变量。
                if (_count <= 30 && (1 << _count) < n)
                {
                    n = 1 << _count;//1向左位移_count
                }
                Thread.SpinWait(n);//是一个循环、n是循环的次数

那么在 10 次调用之后呢?

10 次之后 SpinOnce 就不再进行 Spin 操作了,它根据情况选择进入不同的 Yield 流程。

 

使用场合:

1、只能在进程内的线程使用。

因为他是轻量级锁。轻量级线程同步方案因为没有使用到 Win32 内核对象,而是在 .NET 内部完成,所以只能进行线程之间的同步,不能进行跨进程同步。如果要完成跨进程的同步,需要使用 MonitorMutex 这样的方案。

2、适合在非常轻量的计算中使用。

它与普通 lock 的区别在于普通 lock 使用 Win32 内核态对象来实现等待

 使用要点:

1、如果等待某个条件满足需要的时间很短(几毫秒),而且不希望发生昂贵的上下文切换,那么基于自旋的等待是一种很好的替换方案,SpinWait不仅提供了基本自旋功能,而且还提供了SpinWait.SpinUntil方法,使用这个方法能够自旋直到满足某个条件为止
2、SpinWait 是一种值类型,从内存的角度上说,开销很小。,这意味着低级别代码可以利用 SpinWait 而不用担心不必要的分配开销。
3、SpinWait 并不广泛适用于普通应用程序。 在大多数情况下,应使用 .NET Framework 提供的同步类,如  Monitor。 在需要旋转等待的大多数情况下,SpinWait 结构应该优于  Thread.SpinWait() 方法。
4、SpinWait 也会生成时间片,以防等待线程阻止优先级较高的线程或垃圾回收器。就是说旋转一定次数后会让出一下cpu,有sleep、yiled的动作。
需要注意的是:长时间的自旋不是很好的做法,因为自旋会阻塞更高级的线程及其相关的任务,还会阻塞垃圾回收机制。
5、SpinWait 提供每次调用 SpinOnce 前都可以检查的 NextSpinWillYield 属性。如果此属性返回 true,启动自己的等待操作。
6、SpinWait并没有设计为让多个任务或线程并发使用,因此多个任务或线程通过SpinWait方法进行自旋,那么每一个任务或线程都应该使用自己的SpinWait实例。
7、适合与底层代码库,不适合日常使用。

使用案例:

using System.Diagnostics;
using System.Reflection;

class Program
{

    static SpinLock sl = new();
    static void Main(string[] args)
    {

        
      
        Stopwatch stopwatch = new ();
        SpinWait sw = new();
  //每次自旋的时间
        for (int i = 0; i <40; i++)
        {
          
          //  sw.Reset();// SpinWait内部是 采用count 累计计数的,所以每次使用都要清零
            stopwatch.Reset();
            stopwatch.Start();
           
            sw.SpinOnce(31);
           
            stopwatch.Stop();
            
            Console.WriteLine(stopwatch.ElapsedTicks+sw.NextSpinWillYield.ToString()+"count:"+sw.Count);
        }


    }
}

SpinOnce()执行一次是大概7个时钟周期。第一例外,第一次的时候比较耗时。

 案例一、下面的基本示例展示了无锁堆栈中的 SpinWait。 如果需要高性能的线程安全堆栈,请考虑使用 System.Collections.Concurrent.ConcurrentStack<T>

详解:启用3个线程给自定义堆栈LockFreeStack<T>的 字段reeStac 添加数据(0-20)。用到cas 技术保证了线程的同步

LockFreeStack<int> reeStac = new();

for (int i = 1; i <=3; i++)
{
    Thread se = new Thread(test);
    se.Start();
}

    void test(){


    for (int i = 0; i < 20; i++)
    {
        reeStac.Push(i);

    }

}


public class LockFreeStack<T>
{
    private volatile Node m_head;

    private class Node { public Node Next; public T Value; }

    public void Push(T item)
    {
        var spin = new SpinWait();
        Node node = new Node { Value = item }, head ;
        while (true)
        {
          
            head = m_head;
            node.Next = head;
            Console.WriteLine("Processor:{0},Thread{1},priority:{2} count:{3} ", Thread.GetCurrentProcessorId(), Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.Priority,item );
            Node dd = Interlocked.CompareExchange(ref m_head, node, head);//如果相等 就把node赋值给m_head,返回值都是原来的m_head。
            if (dd == head) break;//判断是否赋值成功。成功就跳出死循环。
            spin.SpinOnce();
            Console.WriteLine("Processor:{0},Thread{1},priority:{2}  spin.SpinOnce()", Thread.GetCurrentProcessorId(), Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.Priority);
        }
    }

    public bool TryPop(out T result)
    {
        result = default(T);
        var spin = new SpinWait();

        Node head;
        while (true)
        {
           
            head = m_head;
            if (head == null) return false;
            if (Interlocked.CompareExchange(ref m_head, head.Next, head) == head)
            {
                result = head.Value;
                return true;
            }
            spin.SpinOnce();  //这边使用了spinwait 结构
        }
    }
}

 

 



案例 :用 Thread.SpinWait 模仿SpinWait.Once()的前10次。

using System.Diagnostics;
using System.Reflection;

class Program
{

    static SpinLock sl = new();
    static void Main(string[] args)
    {

        Type type = typeof(Thread);
        BindingFlags flag =     BindingFlags.Instance | BindingFlags.NonPublic;
     MemberInfo[] inof = type.GetMember("OptimalMaxSpinWaitsPerSpinIteration", BindingFlags.NonPublic);
      
        
        Stopwatch stopwatch = new ();
        SpinWait sw = new();
 
        for (int i = 0; i < 20; i++)
        {
            int ss = 1 << i;
        
            stopwatch.Reset();
            stopwatch.Start();
          
            Thread.SpinWait(ss);
            stopwatch.Stop();
            
            Console.WriteLine(stopwatch.ElapsedTicks+sw.NextSpinWillYield.ToString()+"count:"+ ss);
        }


    }
}

 

posted @ 2022-01-01 04:28  小林野夫  阅读(768)  评论(0编辑  收藏  举报
原文链接:https://www.cnblogs.com/cdaniu/