【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;//获取成功退出⾃旋式的等待writeline特点
}
}
// 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、在某些情况下,会停⽌旋转,以防出现逻辑处理器资源不⾜或超线程系统上优先级反转的情况。
使⽤场合:
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变量浪费时间。单线程数据就在寄存器中,运算速度不受到资源,以最快速度计算。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论