【C# 锁】 SpinLock锁 详细分析(包括内部代码)
OverView
同步基元分为用户模式和内核模式
用户模式:Iterlocked.Exchange(互锁)、SpinLocked(自旋锁)、易变构造(volatile关键字、volatile类、Thread.VolatitleRead|Thread.VolatitleWrite)、MemoryBarrier。
通过对SpinLock锁的内部代码分析,彻底了解SpinLock的工作原理。
SpinLock内部有一个共享变量 owner 表示锁的所有者是谁。当该锁有所有者时,owner不在为0。当owner为0时,表示该锁没有拥有者。任何线程都可以参与竞争该锁。
获取锁的采用的是位逻辑运算,这也是常用的权限运算方式。
锁住其他线程采用的是死循模式,只有满足一定条件才能跳出死循。当第一个线程获取锁的时候。后续进入的线程都会被困在死循环里面,做spinner.SpinOnce()自旋,这是很消耗cpu的,因此SplinLock 锁只能 用于短时间的运算。
锁的内部 没有使用到 Win32 内核对象,所以只能进行线程之间的同步,不能进行跨进程同步。如果要完成跨进程的同步,需要使用 Monitor、Mutex 这样的方案。
通过源代码分析我们可以总结出SpinLock锁的特点: 互斥 、自旋、非重入、只能用于极短暂的运算,进程内使用。
SpinLock锁虽然是值类型,但是内部状态会改变,所以不要把他声明为Readonly字段。
SpinLock锁 的内部构造分析
变量
private volatile int _owner; //多线程共享变量 所以volatile关键字
private const int SLEEP_ONE_FREQUENCY = 40;//自旋多少次以后,执行sleep(1),
private const int TIMEOUT_CHECK_FREQUENCY = 10; // After how many yields, check the timeout //禁用ID 跟踪 性能模式:当高位为1时,锁可用性由低位表示。当低位为1时——锁被持有;0——锁可用。
private const int LOCK_ID_DISABLE_MASK = unchecked((int)0x80000000); // 1000 0000 0000 0000 0000 0000 0000 0000
private const int ID_DISABLED_AND_ANONYMOUS_OWNED = unchecked((int)0x80000001); // 1000 0000 0000 0000 0000 0000 0000 0001 //除非在构造函数时,传入false。否则默认启用线程id跟踪
//启用ID跟踪 启用所有权跟踪模式:高位为0,剩余位为存储当前所有者的托管线程ID。当31位低是0,锁是可用的。
private const int WAITERS_MASK = ~(LOCK_ID_DISABLE_MASK | 1); // 0111 1111 1111 1111 1111 1111 1111 1110
private const int LOCK_ANONYMOUS_OWNED = 0x1; // 0000 0000 0000 0000 0000 0000 0000 0001
构造函数
//除非在初始化时候给构造函数传入false。用默认构造函数初始化或者传入true 都是启用线程id跟踪
public SpinLock(bool enableThreadOwnerTracking)
{
_owner = LOCK_UNOWNED; // 0000 0000 0000 0000 0000 0000 0000 0000
if (!enableThreadOwnerTracking)
{
_owner |= LOCK_ID_DISABLE_MASK; // 1000 0000 0000 0000 0000 0000 0000 0000
Debug.Assert(!IsThreadOwnerTrackingEnabled, "property should be false by now");
}
}
Enter(bool)方法
public void Enter(ref bool lockTaken)
{
// Try to keep the code and branching in this method as small as possible in order to inline the method
int observedOwner = _owner;
if (lockTaken || // invalid parameter 刚开始锁都是未启用的,所以该值都是false
////除非在构造函数时,传入false。否则默认启用线程id跟踪
// 构造函数传入true或者用默认构造函数时候启用线程id跟踪
// observedOwner & ID_DISABLED_AND_ANONYMOUS_OWNED= 0000 0000 0000 0000 0000 0000 0000 0000& 1000 0000 0000 0000 0000 0000 0000 0001
// 当构造函数传入false。
// observedOwner & ID_DISABLED_AND_ANONYMOUS_OWNED= 1000 0000 0000 0000 0000 0000 0000 0000&1000 0000 0000 0000 0000 0000 0000 0001 (observedOwner & ID_DISABLED_AND_ANONYMOUS_OWNED) != LOCK_ID_DISABLE_MASK || //一般情况下是false,构造函数传入false情况下它是ture 。
// 构造函数传入true或者用默认构造函数时候启用线程id跟踪
//observedOwner | LOCK_ANONYMOUS_OWNED=0000 0000 0000 0000 0000 0000 0000 0000| 0000 0000 0000 0000 0000 0000 0000 0001
// 当构造函数传入false。
//observedOwner | LOCK_ANONYMOUS_OWNED=1000 0000 0000 0000 0000 0000 0000 0000| 0000 0000 0000 0000 0000 0000 0000 0001
//用到cas机制,这就是为什么说spinlock是乐观锁
CompareExchange(ref _owner, observedOwner | LOCK_ANONYMOUS_OWNED, observedOwner, ref lockTaken) != observedOwner) //结果为true时候,获取锁失败。
ContinueTryEnter(Timeout.Infinite, ref lockTaken); // Timeout.Infinite=-1 一个用于指定无限长等待时间的常数 如果获取锁失败,就进入自旋等待
}
ContinueTryEnter 方法
//其他代码
//跟踪锁的持有者 (_owner & LOCK_ID_DISABLE_MASK) == 0; 除非构造函数传入false ,否则都走这个分支
if (IsThreadOwnerTrackingEnabled)
{
// Slow path for enabled thread tracking mode
ContinueTryEnterWithThreadTracking(millisecondsTimeout, startTime, ref lockTaken);
return;
}
//其他代码
ContinueTryEnterWithThreadTracking 方法
核心函数
private void ContinueTryEnterWithThreadTracking(int millisecondsTimeout, uint startTime, ref bool lockTaken)
{
Debug.Assert(IsThreadOwnerTrackingEnabled); const int LockUnowned = 0; int newOwner = Environment.CurrentManagedThreadId; if (_owner == newOwner)
{
//防止锁重入 throw new LockRecursionException(SR.SpinLock_TryEnter_LockRecursionException);
} SpinWait spinner = default; // Loop until the lock has been successfully acquired or, if specified, the timeout expires.
while (true)
{
// We failed to get the lock, either from the fast route or the last iteration
// and the timeout hasn't expired; spin once and try again.
spinner.SpinOnce(); // Test before trying to CAS, to avoid acquiring the line exclusively unnecessarily.
//判断锁释放释放了
if (_owner == LockUnowned)
{
//如果释放了就立即获取锁。
if (CompareExchange(ref _owner, newOwner, LockUnowned, ref lockTaken) == LockUnowned)
{
return;//获取成功 退出自旋式的等待
}
}
// Check the timeout. We only RDTSC if the next spin will yield, to amortize the cost.
if (millisecondsTimeout == 0 ||
(millisecondsTimeout != Timeout.Infinite && spinner.NextSpinWillYield &&
TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout) <= 0))
{
return;
}
}
}
EXIT()
public void Exit()
{
// This is the fast path for the thread tracking is disabled, otherwise go to the slow path
if ((_owner & LOCK_ID_DISABLE_MASK) == 0)//默认的构造函数初始化的spinlock 走这一步分支
ExitSlowPath(true);
else
Interlocked.Decrement(ref _owner);//SpinLock(false)的构造函数初始化的spinlock 走这一步分支
} /// </exception>
public void Exit(bool useMemoryBarrier)
{ int tmpOwner = _owner;
if ((tmpOwner & LOCK_ID_DISABLE_MASK) != 0 & !useMemoryBarrier)
{
//退出对锁所有权
_owner = tmpOwner & (~LOCK_ANONYMOUS_OWNED);
}
else
{
//用原子操作的方式 退出锁。因为只有一个线程获取到锁,所以这一般不用这种方式退出,比较耗时。
ExitSlowPath(useMemoryBarrier);
}
}
通过以上代码我们可以总结出SpinLock锁的特点: 互斥 、自旋、非重入、只能用于极短暂的运算。
假如开启4个线程 数数,从0数到1千万,这个程序在4核cpu上运行,其中用了interlock锁 那么运行情况如下图:

此时线程1获得锁,其他线程未获得锁都在自旋中(死循环),占着core不放。所以要确保interLock锁任何线程持有锁的时间不会超过一个非常短的时间段。要不就造成资源巨大浪费。
SpinLock内部使用spinWait、InterLocked实现原子操作。
原理:

锁定内部式SpinWait.SpinOnce。在自旋次数超过10之后,每次进行自旋便会触发上下文切换的操作,在这之后每自旋5次会进行一次sleep(0)操作,每20次会进行一次sleep(1)操作。
Sleep(0) 只允许那些优先级相等或更高的线程使用当前的CPU,其它线程只能等着挨饿了。如果没有合适的线程,那当前线程会重新使用 CPU 时间片。
使用要点:
1、每次使用都要初始化为false 确保未被获取,如果已获取锁,则为 true,否则为 false。
2、SpinLock 是非重入锁,这意味着,如果线程持有锁,则不允许再次进入该锁。
3、SpinLock结构是一个低级别的互斥同步基元,它在等待获取锁时进行旋转。
4、用 SpinLock 时,请确保任何线程持有锁的时间不会超过一个非常短的时间段,并确保任何线程在持有锁时不会阻塞。
5、 即使 SpinLock 未获取锁,它也会产生线程的时间片。此时的未获取锁的线程就是占着cpu的其他core 等着,已经占用锁的线程释放锁。
6、 在多核计算机上,当等待时间预计较短且极少出现争用情况时,SpinLock 的性能将高于其他类型的锁。
7、由于 SpinLock 是一个值类型,因此,如果您希望两个副本都引用同一个锁,则必须通过引用显式传递该锁。
8、如果调用时 Exit 没有首先调用的 Enter 内部状态,则 SpinLock 可能会损坏。
9、如果启用了线程所有权跟踪 (通过) 是否可以使用它 IsThreadOwnerTrackingEnabled ,则当某个线程尝试重新进入它已经持有的锁时,将引发异常。 但是,如果禁用了线程所有权跟踪,尝试输入已持有的锁将导致死锁。
10、SpinLock每次请求同步锁的效率非常高,但如果请求不到的话,会一直请求而浪费CPU时间,所以它适合那种并发程度不高、竞争性不强的场景。
11、在某些情况下,SpinLock 会停止旋转,以防出现逻辑处理器资源不足或超线程系统上优先级反转的情况。
使用场合:
1、只能在进程内的线程使用。
因为他是轻量级锁。轻量级线程同步方案因为没有使用到 Win32 内核对象,而是在 .NET 内部完成,所以只能进行线程之间的同步,不能进行跨进程同步。如果要完成跨进程的同步,需要使用 Monitor、Mutex 这样的方案。
2、适合在非常轻量的计算中使用。
它与普通 lock 的区别在于普通 lock 使用 Win32 内核态对象来实现等待
属性 描述
IsHeld 获取锁当前是否已由任何线程占用。
IsHeldByCurrentThread 获取锁是否已由当前线程占用。
IsThreadOwnerTrackingEnabled 获取是否已为此实例启用了线程所有权跟踪。
方法 描述
Enter(Boolean) 采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 lockTaken 以确定是否已获取锁。
Exit() 释放锁。
Exit(Boolean) 释放锁。
TryEnter(Boolean) 尝试采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 lockTaken 以确定是否已获取锁
TryEnter(Int32, Boolean) 尝试采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 lockTaken 以确定是否已获取锁。
TryEnter(TimeSpan, Boolean) 尝试采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 lockTaken 以确定是否已获取锁。
案例:
开4个线程 从0数到1千万
using System.Diagnostics; class Program
{
static long counter = 1;
//如果声明为只读字段,会导致每次调用都会返回一个SpinLock新副本,
//在多线程下,每个方法都会成功获得锁,而受到保护的临界区不会按照预期进行串行化。
static SpinLock sl = new();//一个类申请一把锁给多线程用,不能声明成只读的。 // 开4个线程 从0数到1千万
static void Main(string[] args)
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
Parallel.Invoke(f1, f1, f1, f1);
Console.WriteLine(stopwatch.ElapsedMilliseconds);
Console.WriteLine(counter); }
static void f1()
{ for (int i = 1; i <= 25_000_00; i++)
{ // static SpinLock sl = new();错误声明方式,这样每个线程都会获得一把锁,导致失去同步的效果
bool dfdf = false;//每次使用都要初始化为false,每一次循环都是开始争抢锁。
sl.Enter(ref dfdf);
try
{
counter++; }
finally
{ sl.Exit();
} }
} }
注意:多线程数数 的效率比单线程还慢。原因是抢锁浪费时间和Volatile变量 浪费时间。单线程数据就在寄存器中,运算速度不受到资源,以最快速度计算。
【C# 锁】 SpinLock锁 详细分析(包括内部代码)的更多相关文章
- Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级
原文链接:https://blog.csdn.net/tongdanping/article/details/79647337 1.锁升级锁的4中状态:无锁状态.偏向锁状态.轻量级锁状态.重量级锁状态 ...
- JUC锁框架_AbstractQueuedSynchronizer详细分析
AQS是JUC锁框架中最重要的类,通过它来实现独占锁和共享锁的.本章是对AbstractQueuedSynchronizer源码的完全解析,分为四个部分介绍: CLH队列即同步队列:储存着所有等待 ...
- 操作系统下spinlock锁解析、模拟及损耗分析
关于spinlock 我们在知道什么是spinlock之前,还需要知道为什么需要这个spinlock?spinlock本质就是锁,提到锁,我们就回到了多线程编程的混沌初期,为了实现多线程编程,操作系统 ...
- 分析SIX锁和锁分区导致的死锁
什么是SIX锁? 官方文档锁模式中说到: 意向排他共享 (SIX):保护针对层次结构中某些(而并非所有)低层资源请求或获取的共享锁以及针对某些(而并非所有)低层资源请求或获取的意向排他锁. 顶级资源允 ...
- 利用多写Redis实现分布式锁原理与实现分析(转)
利用多写Redis实现分布式锁原理与实现分析 一.关于分布式锁 关于分布式锁,可能绝大部分人都会或多或少涉及到. 我举二个例子:场景一:从前端界面发起一笔支付请求,如果前端没有做防重处理,那么可能 ...
- (转)MySQL优化笔记(八)--锁机制超详细解析(锁分类、事务并发、引擎并发控制)
当一个系统访问量上来的时候,不只是数据库性能瓶颈问题了,数据库数据安全也会浮现,这时候合理使用数据库锁机制就显得异常重要了. 原文:http://www.jianshu.com/p/163c96983 ...
- 改进动态设置query cache导致额外锁开销的问题分析及解决方法-mysql 5.5 以上版本
改进动态设置query cache导致额外锁开销的问题分析及解决方法 关键字:dynamic switch for query cache, lock overhead for query cach ...
- 内部锁之一:锁介绍(偏向锁 & 轻量级锁 & 重量级锁 & 各自优缺点及场景)
一.内部锁介绍 上篇文章<Synchronized之二:synchronized的实现原理>中向大家介绍了Synchronized原理及优化锁.现在我们应该知道,Synchronized是 ...
- java并发多线程显式锁Condition条件简介分析与监视器 多线程下篇(四)
Lock接口提供了方法Condition newCondition();用于获取对应锁的条件,可以在这个条件对象上调用监视器方法 可以理解为,原本借助于synchronized关键字以及锁对象,配备了 ...
随机推荐
- C#8.0 可空引用类型
介绍 我们的项目代码运行时最频繁的错误之一就是 System.NullReferenceException 异常,c#8.0增加的可为空引用类型就是用来帮助开发者降低甚至消除NULL异常.我们需要注意 ...
- JavaScript如何实现上拉加载,下拉刷新?
转载地址: 面试官:JavaScript如何实现上拉加载,下拉刷新? 一.前言 下拉刷新和上拉加载这两种交互方式通常出现在移动端中 本质上等同于PC网页中的分页,只是交互形式不同 开源社区也有很多优秀 ...
- Power Apps 创建响应式布局
前言 我们都知道Power Apps作为低代码平台,最大的优势就是各个设备之间的兼容性,尤其是自带的响应式布局,非常好用. 这不,我们就为大家分享一下,如何使用Power Apps画布应用,创建响应式 ...
- poj_3190
首先把所有的牛排个序,优先按照起始时间 其次建立一个堆,重载小于号(只可以重载小于号),优先按照右端点的时间排序,大的放下面(sort的时候会放后面),堆顶是结束时间最快的 #include < ...
- 微服务架构 | 10.3 使用 Zipkin 可视化日志追踪
目录 前言 1. Zipkin 基础知识 1.1 Zipkin 链路监控的原理 2. 下载 Zipkin 服务器 2.1 下载 zipkin-server-2.12.9-exec.jar 包 2.2 ...
- ApacheCN JavaScript 译文集(二) 20211123 更新
使用 Meteor 构建单页 Web 应用 零.前言 一.制作 Meteor 应用 二.构建 HTML 模板 三.存储数据和处理集合 四.控制数据流 五.使我们的应用与路由通用 六.保持会话状态 七. ...
- Atcoder ARC-068
A 不难发现从 \(5\) 开始一直往 \(6\) 转再转回来是最优的,直接模拟即可. B 不难发现可以将多余部分直接贪心消去,最后必然会剩下两个或 \(1\) 个多余的数. 如果剩下两个,此时多余的 ...
- 详解git fetch与git pull的区别(实操)
感谢原文作者:R-H-R 原文链接:https://blog.csdn.net/riddle1981/article/details/74938111 git fetch和git pull都可以将远端 ...
- Java Calendar类的使用总结【转】
感谢!原文地址:https://www.cnblogs.com/huangminwen/p/6041168.html Java Calendar类的使用总结 在实际项目当中,我们经常会涉及到对时间的处 ...
- sublime中的emmet插件的使用技巧
1.我要生成一个2行3列,宽300px,高500px的表. table[width=300 height=500]>(tr>td{$}*3)*2