每个在其程序中使用多个线程的程序员都遇到了同步原语。 在.NET上下文中,有很多它们,我不会列出它们,
MSDN已经为我完成了。
我不得不使用许多这些原语,它们完美地帮助完成了任务。 但是在本文中,我想谈一谈桌面应用程序中的常规锁,以及新的(至少对我而言)原语如何出现(可以称为PriorityLock)。
问题
在开发高负载的多线程应用程序时,管理器出现在处理无数线程的地方。 我也是。 这个经理工作了,处理了来自数百个线程的大量请求。 他的一切都很好,但是在通常的锁里面起作用了。
然后有一天,用户(例如,我)单击应用程序界面中的按钮,该信息流飞向经理(当然不是UI流),并希望看到超级友好的接待,但相反,他是最稠密诊所最稠密的接待处的克拉瓦姨妈遇到的,上面写着“我不愿该死”。谁指导你的。 我还有950个像您一样。 去找他们。 我不在乎你怎么想。” 这就是.NET中锁的工作方式。 一切似乎都很好,一切都将正确执行,但是用户显然不打算等待几秒钟来对其动作做出响应。
这是令人心碎的故事的结尾,技术问题的解决方案开始了。
解决方案
在研究了标准原语之后,我没有找到合适的选择。 因此,我决定写我的锁,它将有一个标准的高优先级条目。 顺便说一句,写完书后,我也学习了nuget,尽管我可能搜索不佳,但在那儿却找不到类似的东西。
要编写这样的原语(或不再是原语),我需要SemaphoreSlim,SpinWait和Interlocked操作。 在剧透中,我引用了PriorityLock的第一个版本(仅同步代码,但这是最重要的),并对此进行了解释。
隐藏文字在同步方面,没有发现,当有人处于锁定状态时,其他人无法进入。 如果出现了高优先级,那么所有等待低优先级的人都会将其推向前进。
LockMgr类,建议在您的代码中使用它。 正是他才是同步的对象。 创建Locker和HighLocker对象,其中包含信号量,SpinWait,希望进入关键部分的计数器,当前线程和递归计数器。
public class LockMgr { internal int HighCount; internal int LowCount; internal Thread CurThread; internal int RecursionCount; internal readonly SemaphoreSlim Low = new SemaphoreSlim(1); internal readonly SemaphoreSlim High = new SemaphoreSlim(1); internal SpinWait LowSpin = new SpinWait(); internal SpinWait HighSpin = new SpinWait(); public Locker HighLock() { return new HighLocker(this); } public Locker Lock(bool high = false) { return new Locker(this, high); } }
Locker类实现IDisposable接口。 为了在捕获锁时实现递归,我们记住流的ID,然后对其进行检查。 此外,根据优先级,在高优先级的情况下,我们立即说我们来了(增加了HighCount计数器),获得了高信号量,然后等待(如有必要)从低优先级中释放锁,然后我们准备好获得锁了。 在低优先级的情况下,低信号量会得到,然后我们等待所有高优先级流的完成,然后在高信号量下使用一会儿,增加LowCount。
值得一提的是,HighCount和LowCount的含义是不同的,HighCount显示进入锁的优先级线程的数量,而LowCount仅表示具有低优先级的线程(一个)进入锁。
public class Locker : IDisposable { private readonly bool _isHigh; private LockMgr _mgr; public Locker(LockMgr mgr, bool isHigh = false) { _isHigh = isHigh; _mgr = mgr; if (mgr.CurThread == Thread.CurrentThread) { mgr.RecursionCount++; return; } if (_isHigh) { Interlocked.Increment(ref mgr.HighCount); mgr.High.Wait(); while (Interlocked.CompareExchange(ref mgr.LowCount, 0, 0) != 0) mgr.HighSpin.SpinOnce(); } else { mgr.Low.Wait(); while (Interlocked.CompareExchange(ref mgr.HighCount, 0, 0) != 0) mgr.LowSpin.SpinOnce(); try { mgr.High.Wait(); Interlocked.Increment(ref mgr.LowCount); } finally { mgr.High.Release(); } } mgr.CurThread = Thread.CurrentThread; } public void Dispose() { if (_mgr.RecursionCount > 0) { _mgr.RecursionCount--; _mgr = null; return; } _mgr.RecursionCount = 0; _mgr.CurThread = null; if (_isHigh) { _mgr.High.Release(); Interlocked.Decrement(ref _mgr.HighCount); } else { _mgr.Low.Release(); Interlocked.Decrement(ref _mgr.LowCount); } _mgr = null; } } public class HighLocker : Locker { public HighLocker(LockMgr mgr) : base(mgr, true) { } }
使用LockMgr类对象非常简洁。 该示例清楚地显示了在关键部分内重用_lockMgr的可能性,而优先级不再重要。
private PriorityLock.LockMgr _lockMgr = new PriorityLock.LockMgr(); public void LowPriority() { using (_lockMgr.Lock()) { using (_lockMgr.HighLock()) {
所以我解决了我的问题。 开始高度优先地处理用户动作,没有人受伤,所有人都赢了。
异步性
由于SemaphoreSlim类的对象支持异步等待,因此我也给自己增加了这个机会。 代码之间的差异很小,在本文的结尾,我将提供源代码的链接。
重要的是,在这里Task不会以任何方式连接到线程,因此,锁的异步重用不能以类似的方式实现。 此外,MSDN描述的
Task.CurrentId属性不能保证任何事情。 这是我的选择结束的地方。
在寻找解决方案时,我遇到了
NeoSmart.AsyncLock项目,在该项目的说明中指出了对重用异步锁的支持。 从技术上讲,重用是可行的。 但不幸的是,锁本身不是锁。 如果使用此软件包,请当心,它不能正常工作!
结论
结果是一个类,该类支持带重用的同步操作和不带重用的异步操作。 异步和同步操作可以并排使用,但不能一起使用! 都是由于缺乏对重用异步选项的支持。
我希望我并不孤单,这样的解决方案对某人有用。 我将库发布在github和nuget上。
存储库中有一些测试可以显示PriorityLock的运行状况。 在此测试的异步部分,已测试NeoSmart.AsyncLock,但测试失败。
链接到nugetGithub链接