C#多线程编程中的锁系统(四):自旋锁
目录
一:基础
二:自旋锁示例
三:SpinLock
四:继续SpinLock
五:总结
一:基础
内核锁:基于内核对象构造的锁机制,就是通常说的内核构造模式。用户模式构造和内核模式构造
优点:cpu利用最大化。它发现资源被锁住,请求就排队等候。线程切换到别处干活,直到接受到可用信号,线程再切回来继续处理请求。
缺点:托管代码->用户模式代码->内核代码损耗、线程上下文切换损耗。
在锁的时间比较短时,系统频繁忙于休眠、切换,是个很大的性能损耗。
自旋锁:原子操作+自循环。通常说的用户构造模式。 线程不休眠,一直循环尝试对资源访问,直到可用。
优点:完美解决内核锁的缺点。
缺点:长时间一直循环会导致cpu的白白浪费,高并发竞争下、CPU的消耗特别严重。
混合锁:内核锁+自旋锁。 混合锁是先自旋锁一段时间或自旋多少次,再转成内核锁。
优点:内核锁和自旋锁的折中方案,利用前二者优点,避免出现极端情况(自旋时间过长,内核锁时间过短)。
缺点: 自旋多少时间、自旋多少次,这些策略很难把控。
ps:操作系统或net框架,这块算法策略做的已经非常优了,有些API函数也提供了时间及次数可配置项,让开发者根据需求自行判断。
二:自旋锁示例
来看下我们自己简单实现的自旋锁:
int signal = 0;
var li = new List<int>();
Parallel.For(0, 1000 * 10000, r =>
{
while (Interlocked.Exchange(ref signal, 1) != 0)//加自旋锁
{
//黑魔法
}
li.Add(r);
Interlocked.Exchange(ref signal, 0); //释放锁
});
Console.WriteLine(li.Count);
//输出:10000000
上面就是自旋锁:Interlocked.Exchange+while
1:定义signal 0可用,1不可用。
2:Parallel模拟并发竞争,原子更改signal状态。 后续线程自旋访问signal,是否可用。
3:A线程使用完后,更改signal为0。 剩余线程竞争访问资源,B线程胜利后,更改signal为1,失败线程继续自旋,直到可用。
三:SpinLock
SpinLock是net4.0后系统帮我们实现的自旋锁,内部做了优化。
简单看下实例:
var li = new List<int>();
var sl = new SpinLock();
Parallel.For(0, 1000 * 10000, r =>
{
bool gotLock = false; //释放成功
sl.Enter(ref gotLock); //进入锁
li.Add(r);
if (gotLock) sl.Exit(); //释放
});
Console.WriteLine(li.Count);
//输出:10000000
四:继续SpinLock
new SpinLock(false) 这个构造函数主要用来帮我们检查死锁用,true是开启。
开启状态下,如果发生死锁会直接抛异常的。
贴了一部分源码(已折叠),我们来看下:
public void Enter(ref bool lockTaken)
{
if (lockTaken)
{
lockTaken = false;
throw new System.ArgumentException(Environment.GetResourceString("SpinLock_TryReliableEnter_ArgumentException"));
}
// Fast path to acquire the lock if the lock is released
// If the thread tracking enabled set the new owner to the current thread id
// Id not, set the anonymous bit lock
int observedOwner = m_owner;
int newOwner = 0;
bool threadTrackingEnabled = (m_owner & LOCK_ID_DISABLE_MASK) == 0;
if (threadTrackingEnabled)
{
if (observedOwner == LOCK_UNOWNED)
newOwner = Thread.CurrentThread.ManagedThreadId;
}
else if ((observedOwner & LOCK_ANONYMOUS_OWNED) == LOCK_UNOWNED)
{
newOwner = observedOwner | LOCK_ANONYMOUS_OWNED; // set the lock bit
}
if (newOwner != 0)
{
#if !FEATURE_CORECLR
Thread.BeginCriticalRegion();
#endif
#if PFX_LEGACY_3_5
if (Interlocked.CompareExchange(ref m_owner, newOwner, observedOwner) == observedOwner)
{
lockTaken = true;
return;
}
#else
if (Interlocked.CompareExchange(ref m_owner, newOwner, observedOwner, ref lockTaken) == observedOwner)
{
// Fast path succeeded
return;
}
#endif
#if !FEATURE_CORECLR
Thread.EndCriticalRegion();
#endif
}
//Fast path failed, try slow path
ContinueTryEnter(Timeout.Infinite, ref lockTaken);
}
private void ContinueTryEnter(int millisecondsTimeout, ref bool lockTaken)
{
long startTicks = 0;
if (millisecondsTimeout != Timeout.Infinite && millisecondsTimeout != 0)
{
startTicks = DateTime.UtcNow.Ticks;
}
#if !FEATURE_PAL && !FEATURE_CORECLR // PAL doesn't support eventing, and we don't compile CDS providers for Coreclr
if (CdsSyncEtwBCLProvider.Log.IsEnabled())
{
CdsSyncEtwBCLProvider.Log.SpinLock_FastPathFailed(m_owner);
}
#endif
if (IsThreadOwnerTrackingEnabled)
{
// Slow path for enabled thread tracking mode
ContinueTryEnterWithThreadTracking(millisecondsTimeout, startTicks, 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 increaes 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;
//***Step 1, take the lock or update the waiters
// try to acquire the lock directly if possoble or update the waiters count
SpinWait spinner = new SpinWait();
while (true)
{
observedOwner = m_owner;
if ((observedOwner & LOCK_ANONYMOUS_OWNED) == LOCK_UNOWNED)
{
#if !FEATURE_CORECLR
Thread.BeginCriticalRegion();
#endif
#if PFX_LEGACY_3_5
if (Interlocked.CompareExchange(ref m_owner, observedOwner | 1, observedOwner) == observedOwner)
{
lockTaken = true;
return;
}
#else
if (Interlocked.CompareExchange(ref m_owner, observedOwner | 1, observedOwner, ref lockTaken) == observedOwner)
{
return;
}
#endif
#if !FEATURE_CORECLR
Thread.EndCriticalRegion();
#endif
}
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 || Interlocked.CompareExchange(ref m_owner, observedOwner + 2, observedOwner) == observedOwner)
break;
spinner.SpinOnce();
}
// Check the timeout.
if (millisecondsTimeout == 0 ||
(millisecondsTimeout != Timeout.Infinite &&
TimeoutExpired(startTicks, millisecondsTimeout)))
{
DecrementWaiters();
return;
}
//***Step 2. Spinning
//lock acquired failed and waiters updated
int turn = ((observedOwner + 2) & WAITERS_MASK) / 2;
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)
{
#if !FEATURE_CORECLR
Thread.BeginCriticalRegion();
#endif
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 PFX_LEGACY_3_5
if (Interlocked.CompareExchange(ref m_owner, newOwner, observedOwner) == observedOwner)
{
lockTaken = true;
return;
}
#else
if (Interlocked.CompareExchange(ref m_owner, newOwner, observedOwner, ref lockTaken) == observedOwner)
{
return;
}
#endif
#if !FEATURE_CORECLR
Thread.EndCriticalRegion();
#endif
}
}
}
// Check the timeout.
if (millisecondsTimeout != Timeout.Infinite && TimeoutExpired(startTicks, millisecondsTimeout))
{
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)
{
#if !FEATURE_CORECLR
Thread.BeginCriticalRegion();
#endif
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 PFX_LEGACY_3_5
if (Interlocked.CompareExchange(ref m_owner, newOwner, observedOwner) == observedOwner)
{
lockTaken = true;
return;
}
#else
if (Interlocked.CompareExchange(ref m_owner, newOwner, observedOwner, ref lockTaken) == observedOwner)
{
return;
}
#endif
#if !FEATURE_CORECLR
Thread.EndCriticalRegion();
#endif
}
if (yieldsoFar % SLEEP_ONE_FREQUENCY == 0)
{
Thread.Sleep(1);
}
else if (yieldsoFar % SLEEP_ZERO_FREQUENCY == 0)
{
Thread.Sleep(0);
}
else
{
#if PFX_LEGACY_3_5
Platform.Yield();
#else
Thread.Yield();
#endif
}
if (yieldsoFar % TIMEOUT_CHECK_FREQUENCY == 0)
{
//Check the timeout.
if (millisecondsTimeout != Timeout.Infinite && TimeoutExpired(startTicks, millisecondsTimeout))
{
DecrementWaiters();
return;
}
}
yieldsoFar++;
}
}
/// <summary>
/// decrements the waiters, in case of the timeout is expired
/// </summary>
private void DecrementWaiters()
{
SpinWait spinner = new SpinWait();
while (true)
{
int observedOwner = m_owner;
if ((observedOwner & WAITERS_MASK) == 0) return; // don't decrement the waiters if it's corrupted by previous call of Exit(false)
if (Interlocked.CompareExchange(ref m_owner, observedOwner - 2, observedOwner) == observedOwner)
{
Contract.Assert(!IsThreadOwnerTrackingEnabled); // Make sure the waiters never be negative which will cause the thread tracking bit to be flipped
break;
}
spinner.SpinOnce();
}
}
从代码中发现SpinLock并不是我们简单的实现那样一直自旋,其内部做了很多优化。
1:内部使用了Interlocked.CompareExchange保持原子操作, m_owner 0可用,1不可用。
2:第一次获得锁失败后,继续调用ContinueTryEnter,ContinueTryEnter有三种获得锁的情况。
3:ContinueTryEnter函数第一种获得锁的方式。 使用了while+SpinWait,后续再讲。
4:第一种方式达到最大等待者数量后,命中走第二种。 继续自旋 turn * 100次。100这个值是处理器核数(4, 8 ,16)下最好的。
5:第二种如果还不能获得锁,走第三种。 这种就有点混合构造的意味了,如下:
if (yieldsoFar % 40 == 0)
Thread.Sleep(1);
else if (yieldsoFar % 10 == 0)
Thread.Sleep(0);
else
Thread.Yield();
Thread.Sleep(1) : 终止当前线程,放弃剩下时间片 休眠1毫秒。 退出跟其他线程抢占cpu。当然这个一般会更多,系统无法保证这么细的时间粒度。
Thread.Sleep(0): 终止当前线程,放弃剩下时间片。 但立马还会跟其他线程抢cpu,能不能抢到跟线程优先级有关。
Thread.Yeild(): 结束当前线程。让出cpu给其他准备好的线程。其他线程ok后或没有准备好的线程,继续执行。 跟优先级无关。
Thread.Yeild()还会返回个bool值,是否让出成功。
从源码中,我们可以学到不少编程技巧。 比如我们也可以使用 自旋+Thread.Yeild() 或 while+Thread.Yeild() 等组合。
五:总结
本章谈了自旋锁的基础+楼主的经验。 SpinLock类源码这块,只粗浅理解了下,并没有深究。
测了下SpinLock和自己实现的自旋锁性能对比(并行添加1000w List<int>()),SpinLock是单纯的自旋锁性能2倍以上。
还测了下lock的性能,是系统SpinLock性能的3倍以上。 可见lock内部自旋的效率更高,CLR暂没开源,所以看不到CLR具体实现的代码。
相关文章
C# WinForm多线程解决界面卡死问题的完美解决方案,使用BeginInvoke
问题描述:当我们的界面需要在程序运行中不断更新数据时,当一个textbox的数据需要变化时,为了让程序执行中不出现界面卡死的现像,最好的方法就是多线程来解决一个主线程来创建界...2020-06-24- 我们在使用C#做项目的时候,基本上都需要制作登录界面,那么今天我们就来一步步看看,如果简单的实现登录界面呢,本文给出2个例子,由简入难,希望大家能够喜欢。...2020-06-25
- 这篇文章主要介绍了C# 字段和属性的的相关资料,文中示例代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下...2020-11-03
- 这篇文章主要介绍了C#中截取字符串的的基本方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-03
- 这篇文章主要介绍了C#实现简单的Http请求的方法,以实例形式较为详细的分析了C#实现Http请求的具体方法,需要的朋友可以参考下...2020-06-25
- 本文给大家分享C#连接SQL数据库和查询数据功能的操作技巧,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧...2021-05-17
- 本文主要介绍了C#中new的几种用法,具有很好的参考价值,下面跟着小编一起来看下吧...2020-06-25
使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序)
这篇文章主要介绍了使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25- 这篇文章主要介绍了C#开发Windows窗体应用程序的简单操作步骤,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-04-12
- 这篇文章主要介绍了C#从数据库读取图片并保存的方法,帮助大家更好的理解和使用c#,感兴趣的朋友可以了解下...2021-01-16
- 最近做一个小项目不可避免的需要前端脚本与后台进行交互。由于是在asp.net中实现,故问题演化成asp.net中jiavascript与后台c#如何进行交互。...2020-06-25
- 本文通过例子,讲述了C++调用C#的DLL程序的方法,作出了以下总结,下面就让我们一起来学习吧。...2020-06-25
- 轻松学习C#的基础入门,了解C#最基本的知识点,C#是一种简洁的,类型安全的一种完全面向对象的开发语言,是Microsoft专门基于.NET Framework平台开发的而量身定做的高级程序设计语言,需要的朋友可以参考下...2020-06-25
- 本文主要介绍了C#变量命名规则小结,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-09
- 这篇文章主要介绍了C#绘制曲线图的方法,以完整实例形式较为详细的分析了C#进行曲线绘制的具体步骤与相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25
- 本文主要介绍了C# 中取绝对值的函数。具有很好的参考价值。下面跟着小编一起来看下吧...2020-06-25
- 这篇文章主要介绍了c#自带缓存使用方法,包括获取数据缓存、设置数据缓存、移除指定数据缓存等方法,需要的朋友可以参考下...2020-06-25
- 这篇文章主要介绍了c#中(&&,||)与(&,|)的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-25
- 这篇文章主要用实例讲解C#递归算法的概念以及用法,文中代码非常详细,帮助大家更好的参考和学习,感兴趣的朋友可以了解下...2020-06-25
- 下面小编就为大家带来一篇C#学习笔记- 随机函数Random()的用法详解。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25