锁提供了线程安全三要素中的 有序性。二、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);
} }
}

【C# 锁】SpinWait结构 -同步基元|同步原语的更多相关文章

  1. https://github.com/python/cpython/blob/master/Doc/library/contextlib.rst 被同一个线程多次获取的同步基元组件

    # -*- coding: utf-8 -*- import time from threading import Lock, RLock from datetime import datetime ...

  2. 【C# 线程】 atomic action原子操作|primitive(基元、原语)

    概念 原子操作(atomic action):也叫primitive(原语.基元),它是操作系统用语范畴.指由若干条指令组成的,用于完成一定功能的一个过程.  原语是由若干个机器指令构成的完成某种特定 ...

  3. 基元线程同步构造之 Mutes(互斥体)

    互斥体实现了“互相排斥”(mutual exclusion)同步的简单形式(所以名为互斥体(mutex)). 互斥体禁止多个线程同时进入受保护的代码“临界区”(critical section). 因 ...

  4. 【C#进阶系列】28 基元线程同步构造

    多个线程同时访问共享数据时,线程同步能防止数据损坏.之所以要强调同时,是因为线程同步问题实际上就是计时问题. 不需要线程同步是最理想的情况,因为线程同步一般很繁琐,涉及到线程同步锁的获取和释放,容易遗 ...

  5. 【C#】C#线程_基元线程的同步构造

    目录结构: contents structure [+] 简介 为什么需要使用线程同步 线程同步的缺点 基元线程同步 什么是基元线程 基元用户模式构造和内核模式构造的比较 用户模式构造 易变构造(Vo ...

  6. Clr Via C#读书笔记----基元线程同步构造

    线程文章:http://www.cnblogs.com/edisonchou/p/4848131.html 重点在于多个线程同时访问,保持线程的同步. 线程同步的问题: 1,线程同步比较繁琐,而且容易 ...

  7. [.net]基元线程同步构造

    /* 基元线程同步构造 用户模式构造: 易变构造(Volatile Construct) 互锁构造(Interlocked Construct):自旋锁(Spinlock) 乐观锁(Optimisti ...

  8. 基元线程同步构造之waithandle中 waitone使用

    在使用基元线程同步构造中waithandle中waitone方法的讲解: 调用waithandle的waitone方法阻止当前线程(提前是其状态为Nonsignaled,即红灯),直到当前的 Wait ...

  9. mysql对比表结构对比同步,sqlyog架构同步工具

    mysql对比表结构对比同步,sqlyog架构同步工具 对比后的结果示例: 执行后的结果示例: 点击:"另存为(S)" 按钮可以把更新sql导出来.

随机推荐

  1. CMake语法—普通变量与子目录(Normal Variable And Subdirectory)

    目录 CMake语法-普通变量与子目录(Normal Variable And Subdirectory) 1 CMake普通变量与子目录示例 1.1 代码目录结构 1.2 父目录CMakeLists ...

  2. WebGPU 中消失的 VAO

    1 VAO 是 OpenGL 技术中提出来的 参考: 外链 其中有一段文字记录了 VAO 是什么: A Vertex Array Object (VAO) is an object which con ...

  3. 微信小程序入门教程之四:API 使用

    今天是这个系列教程的最后一篇. 上一篇教程介绍了,小程序页面如何使用 JavaScript 脚本.有了脚本以后,就可以调用微信提供的各种能力(即微信 API),从而做出千变万化的页面.本篇就介绍怎么使 ...

  4. MySQL基本数据类型之枚举与集合类型

    目录 一:枚举 1.枚举 2.创建表(使用枚举) 3.表内添加数据 二:集合 1.集合 2.创建表(使用集合) 3.表内添加数据 一:枚举 1.枚举 枚举作用: 提前定义好数据之后 后续录入只能录定义 ...

  5. cobbler最小化安装centos8

    centos8 已经发布了GA版本,迫不及待的想尝鲜了,然后现实总是那么残酷,一直安装失败,具体安装步骤如下: 假设cobbler已配置完成. 1.下载centos8 iso镜像 wget http: ...

  6. React 世界的一等公民 - 组件

    猪齿鱼Choerodon平台使用 React 作为前端应用框架,对前端的展示做了一定的封装和处理,并配套提供了前端组件库Choerodon UI.结合实际业务情况,不断对组件优化设计,提高代码质量. ...

  7. Spring系列9:基于注解的Spring容器配置

    写在前面 前面几篇中我们说过,Spring容器支持3种方式进行bean定义信息的配置,现在具体说明下: XML:bean的定义和依赖都在xml文件中配置,比较繁杂. Annotation-based ...

  8. Linux 常见文件管理命令

    Linux文件系统 根目录:/ 从根目录开始,下面有一堆小目录 root:根用户的目录 bin:可执行文件命令 etc:配置文件 var:日志 lib:安装包或头文件,库文件 home:所有用户的家目 ...

  9. Github新安全措施:停止Git客户端账号密码登录的解决方案

    今年 8 月 13 日之后,如果你还用账户密码来操作 Github 上的仓库,就会收到如下警告: remote: Support for password authentication was rem ...

  10. 安装JDK,以及配置环境变量

    卸载JDK 删除Java的安装目录 删除JAVA_HOME 删除path下关于Java的目录 Java-version 安装JDK 使用浏览器搜索JDK,找到下载地址 同意协议 下载电脑对应的版本 双 ...