【C# 锁】SpinWait结构 -同步基元|同步原语
锁提供了线程安全三要素中的 有序性。二、SpinWait是同步锁的核心,而Thread是SpinWait的核心。
Spinwait结构
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 内部完成,所以只能进行线程之间的同步,不能进行跨进程同步。如果要完成跨进程的同步,需要使用 Monitor
、Mutex
这样的方案。
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); } } }