OverView

同步基元分为用户模式和内核模式

用户模式:Iterlocked.Exchange(互锁)、SpinLocked(自旋锁)、易变构造(volatile关键字、volatile类、Thread.VolatitleRead|Thread.VolatitleWrite)、MemoryBarrier。

通过对SpinLock锁的内部代码分析,彻底了解SpinLock的工作原理。

SpinLock内部有一个共享变量 owner 表示锁的所有者是谁。当该锁有所有者时,owner不在为0。当owner为0时,表示该锁没有拥有者。任何线程都可以参与竞争该锁。

获取锁的采用的是位逻辑运算,这也是常用的权限运算方式。

锁住其他线程采用的是死循模式,只有满足一定条件才能跳出死循。当第一个线程获取锁的时候。后续进入的线程都会被困在死循环里面,做spinner.SpinOnce()自旋,这是很消耗cpu的,因此SplinLock 锁只能 用于短时间的运算。

锁的内部 没有使用到 Win32 内核对象,所以只能进行线程之间的同步,不能进行跨进程同步。如果要完成跨进程的同步,需要使用 MonitorMutex 这样的方案。

通过源代码分析我们可以总结出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 内部完成,所以只能进行线程之间的同步,不能进行跨进程同步。如果要完成跨进程的同步,需要使用 MonitorMutex 这样的方案。

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锁 详细分析(包括内部代码)的更多相关文章

  1. Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级

    原文链接:https://blog.csdn.net/tongdanping/article/details/79647337 1.锁升级锁的4中状态:无锁状态.偏向锁状态.轻量级锁状态.重量级锁状态 ...

  2. JUC锁框架_AbstractQueuedSynchronizer详细分析

      AQS是JUC锁框架中最重要的类,通过它来实现独占锁和共享锁的.本章是对AbstractQueuedSynchronizer源码的完全解析,分为四个部分介绍: CLH队列即同步队列:储存着所有等待 ...

  3. 操作系统下spinlock锁解析、模拟及损耗分析

    关于spinlock 我们在知道什么是spinlock之前,还需要知道为什么需要这个spinlock?spinlock本质就是锁,提到锁,我们就回到了多线程编程的混沌初期,为了实现多线程编程,操作系统 ...

  4. 分析SIX锁和锁分区导致的死锁

    什么是SIX锁? 官方文档锁模式中说到: 意向排他共享 (SIX):保护针对层次结构中某些(而并非所有)低层资源请求或获取的共享锁以及针对某些(而并非所有)低层资源请求或获取的意向排他锁. 顶级资源允 ...

  5. 利用多写Redis实现分布式锁原理与实现分析(转)

    利用多写Redis实现分布式锁原理与实现分析   一.关于分布式锁 关于分布式锁,可能绝大部分人都会或多或少涉及到. 我举二个例子:场景一:从前端界面发起一笔支付请求,如果前端没有做防重处理,那么可能 ...

  6. (转)MySQL优化笔记(八)--锁机制超详细解析(锁分类、事务并发、引擎并发控制)

    当一个系统访问量上来的时候,不只是数据库性能瓶颈问题了,数据库数据安全也会浮现,这时候合理使用数据库锁机制就显得异常重要了. 原文:http://www.jianshu.com/p/163c96983 ...

  7. 改进动态设置query cache导致额外锁开销的问题分析及解决方法-mysql 5.5 以上版本

    改进动态设置query cache导致额外锁开销的问题分析及解决方法 关键字:dynamic switch for query cache,  lock overhead for query cach ...

  8. 内部锁之一:锁介绍(偏向锁 & 轻量级锁 & 重量级锁 & 各自优缺点及场景)

    一.内部锁介绍 上篇文章<Synchronized之二:synchronized的实现原理>中向大家介绍了Synchronized原理及优化锁.现在我们应该知道,Synchronized是 ...

  9. java并发多线程显式锁Condition条件简介分析与监视器 多线程下篇(四)

    Lock接口提供了方法Condition newCondition();用于获取对应锁的条件,可以在这个条件对象上调用监视器方法 可以理解为,原本借助于synchronized关键字以及锁对象,配备了 ...

随机推荐

  1. [JavaWeb]利用JSP的编码特性制作免杀后门

    利用JSP的编码特性制作免杀后门 这里是借鉴了Y4stacker师傅的thinkings 待解决的问题 JSP解析 JSP"乱码"为什么还能被识别 "乱码"的J ...

  2. thingsboard源码编译启动

    开发环境 不同的版本对应的开发环境不同(这里以3.3.3版本说明) jdk11+:参考jdk11+安装(win) Maven3.6+:Maven安装配置 Git:参考Git安装 IDEA: 参考IDE ...

  3. Ubuntu 14.04更换内核

    1:查看当前安装的内核 dpkg -l|grep linux-image 2:查看可以更新的内核版本: sudo apt-cache search linux-image 3:安装新内核 sudo a ...

  4. Spring专题1: 静态代理和动态代理

    合集目录 Spring专题1: 静态代理和动态代理 为什么需要代理模式? 代理对象处于访问者和被访问者之间,可以隔离这两者之间的直接交互,访问者与代理对象打交道就好像在跟被访者者打交道一样,因为代理者 ...

  5. kibana 对es的简单操作。

    一.查询和查看. #1.查询所有的数据 GET _search { "query": { "match_all":{} } } #2. 查看ES集群的健康状态 ...

  6. tmux 入门教程

    tmux 本教程是基于ACWing的<Linux基础课>所做,希望大家支持ACWing 功能 分屏 当需要同时运行两个终端,并且进行比对着输入时,来回切换比较麻烦,就可以利用分屏 可以在一 ...

  7. PHP获取日期和时间:

    转载请注明来源:https://www.cnblogs.com/hookjc/ 使用函式 date() 实现 <?php echo $showtime=date("Y-m-d H:i: ...

  8. IDEA中的.iml文件和.idea文件夹作用和意义

    感谢原文作者:LZHHuo 原文链接:https://blog.csdn.net/weixin_41699562/article/details/99552780 .iml文件 idea 对modul ...

  9. redis清缓存

    先查询当前redis的服务是否已经启动 ps -ef|grep redis [root@guanbin-k8s-master ~]# ps -ef|grep redis redis 1557 1 0 ...

  10. System 系统类

    import java.util.Arrays; import java.util.Properties; /* System 系统类 主要用于获取系统的属性数据. System类常用的方法: arr ...