关于SpinLock自旋锁网上已经有很多说明,这里也copy了一部分,我这里主要关注微软的实现,学习人家的实现方式。
如果由于垃圾回收,基于对象的锁对象开销太高,可以使用SpinLock结构。.NET 4以后版本可使用,如果你有很多个锁(如,一个列表里面的每一个节点)并且锁时间通常非常的短,使用SpinLock将很有用。你需要避免使用超过一个的SpinLock,并且不要调用任何可能阻塞的。除了架构不同,SpinLock的使用同Monitor类非常相似。通过Enter或者TryEnter请求锁,并通过Exit释放锁。SpinLock同样也通过两个属性来提供关于它当前是否已锁的信息:IsHeld和IsHeldByCurrentThread.
不要将SpinLock声明为只读字段,如果声明为只读字段,会导致每次调用都会返回一个SpinLock新副本,在多线程下,每个方法都会成功获得锁,而受到保护的临界区不会按照预期进行串行化。
SpinLock 仅当您确定这样做可以改进应用程序的性能之后才能使用。另外,务必请注意 SpinLock 是一个值类型(出于性能原因)。因此,您必须非常小心,不要意外复制了 SpinLock 实例,因为两个实例(原件和副本)之间完全独立,这可能会导致应用程序出现错误行为。如果必须传递 SpinLock 实例,则应该通过引用而不是通过值传递。
[HostProtection(Synchronization = true, ExternalThreading = true)]
public struct SpinLock
{
private volatile int m_owner;
private const int SPINNING_FACTOR = 100;
// After how many yields, call Sleep(1)
private const int SLEEP_ONE_FREQUENCY = 40;
// After how many yields, call Sleep(0)
private const int SLEEP_ZERO_FREQUENCY = 10;
// After how many yields, check the timeout
private const int TIMEOUT_CHECK_FREQUENCY = 10;
// Thr thread tracking disabled mask
private const int LOCK_ID_DISABLE_MASK = unchecked((int)0x80000000);
public SpinLock(bool enableThreadOwnerTracking)
{
m_owner = LOCK_UNOWNED;
if (!enableThreadOwnerTracking)
{
m_owner |= LOCK_ID_DISABLE_MASK;
Contract.Assert(!IsThreadOwnerTrackingEnabled, "property should be false by now");
}
}
public void TryEnter(ref bool lockTaken)
{
TryEnter(0, ref lockTaken);
}
public void TryEnter(int millisecondsTimeout, ref bool lockTaken)
{
int observedOwner = m_owner;
if (millisecondsTimeout < -1 || //invalid parameter
lockTaken || //invalid parameter
(observedOwner & ID_DISABLED_AND_ANONYMOUS_OWNED) != LOCK_ID_DISABLE_MASK || //thread tracking is enabled or the lock is already acquired
Interlocked.CompareExchange(ref m_owner, observedOwner | LOCK_ANONYMOUS_OWNED, observedOwner, ref lockTaken) != observedOwner) // acquiring the lock failed
ContinueTryEnter(millisecondsTimeout, ref lockTaken); // The call the slow pth
}
private void ContinueTryEnter(int millisecondsTimeout, ref bool lockTaken)
{
uint startTime = 0;
if (millisecondsTimeout != Timeout.Infinite && millisecondsTimeout != 0)
{
startTime = TimeoutHelper.GetTime();
}
if (IsThreadOwnerTrackingEnabled)
{
// Slow path for enabled thread tracking mode
ContinueTryEnterWithThreadTracking(millisecondsTimeout, startTime, ref lockTaken);
return;
}
// then thread tracking is disabled
// In this case there are three ways to acquire the lock
// 1- the first way the thread either tries to get the lock if it's free or updates the waiters, if the turn >= the processors count then go to 3 else go to 2
// 2- In this step the waiter threads spins and tries to acquire the lock, the number of spin iterations and spin count is dependent on the thread turn
// the late the thread arrives the more it spins and less frequent it check the lock avilability
// Also the spins count is increases each iteration
// If the spins iterations finished and failed to acquire the lock, go to step 3
// 3- This is the yielding step, there are two ways of yielding Thread.Yield and Sleep(1)
// If the timeout is expired in after step 1, we need to decrement the waiters count before returning
int observedOwner;
int turn = int.MaxValue;
//***Step 1, take the lock or update the waiters
// try to acquire the lock directly if possible or update the waiters count
observedOwner = m_owner;
if ((observedOwner & LOCK_ANONYMOUS_OWNED) == LOCK_UNOWNED)
{
if (Interlocked.CompareExchange(ref m_owner, observedOwner | 1, observedOwner, ref lockTaken) == observedOwner)
{
return;
}
}
else //failed to acquire the lock,then try to update the waiters. If the waiters count reached the maximum, jsut break the loop to avoid overflow
{
if ((observedOwner & WAITERS_MASK) != MAXIMUM_WAITERS)
turn = (Interlocked.Add(ref m_owner, 2) & WAITERS_MASK) >> 1 ;
}
// Check the timeout.
if (millisecondsTimeout == 0 ||
(millisecondsTimeout != Timeout.Infinite &&
TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout) <= 0))
{
DecrementWaiters();
return;
}
//***Step 2. Spinning
//lock acquired failed and waiters updated
int processorCount = PlatformHelper.ProcessorCount;
if (turn < processorCount)
{
int processFactor = 1;
for (int i = 1; i <= turn * SPINNING_FACTOR; i++)
{
Thread.SpinWait((turn + i) * SPINNING_FACTOR * processFactor);
if (processFactor < processorCount)
processFactor++;
observedOwner = m_owner;
if ((observedOwner & LOCK_ANONYMOUS_OWNED) == LOCK_UNOWNED)
{
int newOwner = (observedOwner & WAITERS_MASK) == 0 ? // Gets the number of waiters, if zero
observedOwner | 1 // don't decrement it. just set the lock bit, it is zzero because a previous call of Exit(false) ehich corrupted the waiters
: (observedOwner - 2) | 1; // otherwise decrement the waiters and set the lock bit
Contract.Assert((newOwner & WAITERS_MASK) >= 0);
if (Interlocked.CompareExchange(ref m_owner, newOwner, observedOwner, ref lockTaken) == observedOwner)
{
return;
}
}
}
}
// Check the timeout.
if (millisecondsTimeout != Timeout.Infinite && TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout) <= 0)
{
DecrementWaiters();
return;
}
//*** Step 3, Yielding
//Sleep(1) every 50 yields
int yieldsoFar = 0;
while (true)
{
observedOwner = m_owner;
if ((observedOwner & LOCK_ANONYMOUS_OWNED) == LOCK_UNOWNED)
{
int newOwner = (observedOwner & WAITERS_MASK) == 0 ? // Gets the number of waiters, if zero
observedOwner | 1 // don't decrement it. just set the lock bit, it is zzero because a previous call of Exit(false) ehich corrupted the waiters
: (observedOwner - 2) | 1; // otherwise decrement the waiters and set the lock bit
Contract.Assert((newOwner & WAITERS_MASK) >= 0);
if (Interlocked.CompareExchange(ref m_owner, newOwner, observedOwner, ref lockTaken) == observedOwner)
{
return;
}
}
if (yieldsoFar % SLEEP_ONE_FREQUENCY == 0)
{
Thread.Sleep(1);
}
else if (yieldsoFar % SLEEP_ZERO_FREQUENCY == 0)
{
Thread.Sleep(0);
}
else
{
Thread.Yield();
}
if (yieldsoFar % TIMEOUT_CHECK_FREQUENCY == 0)
{
//Check the timeout.
if (millisecondsTimeout != Timeout.Infinite && TimeoutHelper.UpdateTimeOut(startTime, millisecondsTimeout) <= 0)
{
DecrementWaiters();
return;
}
}
yieldsoFar++;
}
}
private void ContinueTryEnterWithThreadTracking(int millisecondsTimeout, uint startTime, ref bool lockTaken)
{
Contract.Assert(IsThreadOwnerTrackingEnabled);
int lockUnowned = 0;
// We are using thread IDs to mark ownership. Snap the thread ID and check for recursion.
// We also must or the ID enablement bit, to ensure we propagate when we CAS it in.
int m_newOwner = Thread.CurrentThread.ManagedThreadId;
if (m_owner == m_newOwner)
{
// We don't allow lock recursion.
throw new LockRecursionException(Environment.GetResourceString("SpinLock_TryEnter_LockRecursionException"));
}
SpinWait spinner = new SpinWait();
// Loop until the lock has been successfully acquired or, if specified, the timeout expires.
do
{
// 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 (m_owner == lockUnowned)
{
if (Interlocked.CompareExchange(ref m_owner, m_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;
}
} while (true);
}
public void Exit()
{
//This is the fast path for the thread tracking is disabled, otherwise go to the slow path
if ((m_owner & LOCK_ID_DISABLE_MASK) == 0)
ExitSlowPath(true);
else
Interlocked.Decrement(ref m_owner);
}
private void ExitSlowPath(bool useMemoryBarrier)
{
bool threadTrackingEnabled = (m_owner & LOCK_ID_DISABLE_MASK) == 0;
if (threadTrackingEnabled && !IsHeldByCurrentThread)
{
throw new System.Threading.SynchronizationLockException(
Environment.GetResourceString("SpinLock_Exit_SynchronizationLockException"));
}
if (useMemoryBarrier)
{
if (threadTrackingEnabled)
Interlocked.Exchange(ref m_owner, LOCK_UNOWNED);
else
Interlocked.Decrement(ref m_owner);
}
else
{
if (threadTrackingEnabled)
m_owner = LOCK_UNOWNED;
else
{
int tmpOwner = m_owner;
m_owner = tmpOwner & (~LOCK_ANONYMOUS_OWNED);
}
}
}
public bool IsHeld
{
get
{
if (IsThreadOwnerTrackingEnabled)
return m_owner != LOCK_UNOWNED;
return (m_owner & LOCK_ANONYMOUS_OWNED) != LOCK_UNOWNED;
}
}
public bool IsHeldByCurrentThread
{
get
{
if (!IsThreadOwnerTrackingEnabled)
{
throw new InvalidOperationException(Environment.GetResourceString("SpinLock_IsHeldByCurrentThread"));
}
return ((m_owner & (~LOCK_ID_DISABLE_MASK)) == Thread.CurrentThread.ManagedThreadId);
}
}
}
SpinLock 构造函数有一个bool enableThreadOwnerTracking参数用来表示是否跟踪线程,如果为true,那么在获取锁以后变量m_owner就是线程ManagedThreadId属性,否者为1,因为获取锁的修改 observedOwner | 1 ,就相当于m_owner设为1,在释放锁的时候m_owner减1Interlocked.Decrement(ref m_owner) 或者设置为 Interlocked.Exchange(ref m_owner, LOCK_UNOWNED)。
SpinLock的核心方法Enter和TryEnter最终都是调用ContinueTryEnter方法,该方法首先检查IsThreadOwnerTrackingEnabled是否启用线程跟踪,如果启用就调用ContinueTryEnterWithThreadTracking方法,ContinueTryEnterWithThreadTracking方法里面实例化了一个SpinWait,然后自旋获取锁,这里也是借用原子操作【Interlocked.CompareExchange(ref m_owner, m_newOwner, lockUnowned, ref lockTaken)】,如果没有启用跟踪,那么ContinueTryEnter将分3不走,就像里面的注释描述的那样;case 1通过原子操作【Interlocked.CompareExchange(ref m_owner, observedOwner | 1, observedOwner, ref lockTaken) == observedOwner)】直接获取锁,如果失败进入到case2【turn < processorCount】,然后在循环尝试获取锁,每次循环都会调用 Thread.SpinWait方法等待;获取锁还是通过原子操作,如果失败,则进入case3,该case也是循环等待,在循环体里面不在是 Thread.SpinWait而是 Thread.Yield();和 Thread.Sleep(0);
if (yieldsoFar % 40 == 0)
Thread.Sleep(1);
else if (yieldsoFar % 10 == 0)
Thread.Sleep(0);
else
Thread.Yield();
看到这个代码 是不是和 SpinWait相似啊。可以总结以下,case1 直接尝试获取锁,case2 循环中通过调用Thread.SpinWait 尝试获取锁【当前线程不会让出CPU】, case3循环中通过Thread.Yield()和Thread.Sleep来尝试获取锁。
Exit的方法实现就非常简单了,主要是调用ExitSlowPath,说白了就是把变量m_owner还原为初始值。