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变量 浪费时间。单线程数据就在寄存器中,运算速度不受到资源,以最快速度计算。

最新文章

  1. .vue文件里引用单独样式和js文件
  2. Python调用HTTP接口并传递cookie
  3. 【转】oracle中in和exists的区别
  4. apache 指定的网络名不再可用 原因及解决方法
  5. 【BZOJ 3669】【NOI 2014】魔法森林 LCT+枚举边
  6. maven在windows环境下加载settings.xml文件
  7. 【M10】在构造方法内阻止资源泄漏
  8. url的三个js编码函数escape(),encodeURI(),encodeURIComponent()简介
  9. 理解cookie的path和domain属性(转)
  10. latex lstlisting
  11. 2017-2018-1 20155306 mypwd的实现
  12. 陪你解读Spring Batch(一)Spring Batch介绍
  13. mysql 严格模式 Strict Mode说明(text 字段不能加默认或者 不能加null值得修改方法)
  14. spark实时计算性能优化
  15. 【python】正则表达式中的转义问题
  16. [NOIp2010提高组]关押罪犯
  17. centos下nginx的启动
  18. 温馨小程序前端布局Flex
  19. BBS-基于forms组件和ajax实现注册功能
  20. MongoDB的Spring-data-mongodb集成(Win10 x64) 第一章 - MongoDB安装与简单命令

热门文章

  1. 根据SVG Arc求出其开始角、摆动角和椭圆圆心
  2. golang中字符串、数值、2进制、8进制、16进制、10进制、日期和字符串之间的转换
  3. java关键字final
  4. python13day
  5. Python初学笔记之字符串
  6. 线程终止的四种方式,interrupt 方法使用的简单介绍。
  7. asyncio和aiohttp
  8. plsql 储存过程 参数的传递方式?
  9. BeanFactory 工厂模式
  10. Serverless Workflow项目