(译)构建Async同步基元,Part 7 AsyncReaderWriterLock
最近在学习.NET4.5关于“并行任务”的使用。“并行任务”有自己的同步机制,没有显示给出类似如旧版本的:事件等待句柄、信号量、lock、ReaderWriterLock……等同步基元对象,但我们可以沿溪这一编程习惯,那么这系列翻译就是给“并行任务”封装同步基元对象。翻译资源来源《(译)关于Async与Await的FAQ》
1. 构建Async同步基元,Part 1 AsyncManualResetEvent
2. 构建Async同步基元,Part 2 AsyncAutoResetEvent
3. 构建Async同步基元,Part 3 AsyncCountdownEvent
4. 构建Async同步基元,Part 4 AsyncBarrier
5. 构建Async同步基元,Part 5 AsyncSemaphore
6. 构建Async同步基元,Part 6 AsyncLock
7. 构建Async同步基元,Part 7 AsyncReaderWriterLock
开始:构建Async同步基元,Part 7 AsyncReaderWriterLock
在上一篇文章中,我们构建了一个AsyncLock,本文,我将构建一个更高级的构造,一个异步版本的读写/锁。
异步版本的读写/锁比前几篇文章中所创建的同步基元要更加复杂。它包含更多的控制,这意味着需要更多决定来构建这个类型的精确行为。对于本例,我做了以下决定。首先,写操作比读操作拥有更高的优先级,即无论读操作或写操作请求的顺序,如果有写操作正在等待,它将得到优先于任何数量的读操作得到处理,即使它比那些读操作请求的晚些。第二,我决定不对读操作限流,即只要此时不存在未解决的写操作或等待着的写操作,所有读操作会立马被唤醒。
这是我们将构建的目标类型:
1 2 3 4 5 6 7 8 9 10 11 12 | public class AsyncReaderWriterLock { public AsyncReaderWriterLock(); public Task<Releaser> ReaderLockAsync(); public Task<Releaser> WriterLockAsync(); public struct Releaser : IDisposable { public void Dispose(); } } |
就像AsyncLock一样,我们使用一个实现IDiposable接口的Releaser结构,使AsyncReaderWriterLock类型能轻易用于一个范围,例如:
1 2 3 4 5 6 | private readonly AsyncReaderWriterLock m_lock = new AsyncReaderWriterLock(); … using ( var releaser = await m_lock.ReaderLockAsync()) { … // protected code here } |
现在构建Releaser结构,我们使用一个bool值类型变量标识是否是写操作,因为我们需要使用不同的方式来唤醒两种操作的锁(优先处理写操作)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public struct Releaser : IDisposable { private readonly AsyncReaderWriterLock m_toRelease; private readonly bool m_writer; internal Releaser(AsyncReaderWriterLock toRelease, bool writer) { m_toRelease = toRelease; m_writer = writer; } public void Dispose() { if (m_toRelease != null ) { if (m_writer) m_toRelease.WriterRelease(); else m_toRelease.ReaderRelease(); } } } |
因为AsyncReaderWriterLock比前几篇文章中所创建的同步基元要更加复杂,所以我需要更多成员变量。首先,我们为了提高性能,我为读操作的等待缓存了一个Task<Releaser>,同时也为写操作的等待缓存了一个Task<Releaser>以便立即完成。
1 2 3 4 5 6 7 | private readonly Task<Releaser> m_readerReleaser; private readonly Task<Releaser> m_writerReleaser; public AsyncReaderWriterLock() { m_readerReleaser = Task.FromResult( new Releaser( this , false )); m_writerReleaser = Task.FromResult( new Releaser( this , true )); } |
接下来,我需要为写操作维护一个等待队列,对于每一个写操作等待者都有对应的Task<CompletionSource>实例,因为我需要支持单独唤醒它们。同样也需要为读操作维护一个TaskCompletionSource<Releaser>实例。然而,对于读操作,按我们之前谈论的设计,当允许读操作运行时,就能让所有读操作一起运行,因此我只需要一个TaskCompletionSource<Releaser>实例。然而,因为我为读操作只维护了一个TaskCompletionSource<Releaser>实例,所以我还需要知道有多少个读操作正在等待,以便当我最终唤醒它们时,我能确保所有读操作被唤醒。
1 2 3 4 5 | private readonly Queue<TaskCompletionSource<Releaser>> m_waitingWriters = new Queue<TaskCompletionSource<Releaser>>(); private TaskCompletionSource<Releaser> m_waitingReader = new TaskCompletionSource<Releaser>(); private int m_readersWaiting; |
最后,我需要一个变量来维护锁的当前状态,它是一个整型,数值为0表示锁空闲;数值为-1表示一个写操作获取锁;正值表示一个或多个读操作获取锁。
1 | private int m_status; |
现在我们来实现4个方法:ReaderLockAsync, ReaderRelease, WriterLockAsync, and WriterRelease。(为了保证这4个方法数据的同步,我们首先对m_waitingWriters加锁)
ReaderLockAsync()方法用于读操作获取锁。在对m_waitingWriters加独占锁后,我们需要决定读操作是应该立即响应还是应该被迫等待。正如前面的决策,如果当前不存在等待的写操作或正在执行写操作,则读操作能立即得到响应,这种情况下,我们递增m_status计数并且返回为读操作缓存的TaskCompletionSource<Releaser>实例。如果当前存在等待的写操作或正在执行写操作,则我们强迫读操作等待,我们递增m_readersWaiting计数并且为读操作缓存的TaskCompletionSource<Releaser>实例添加延续任务并返回(延续任务确保所有的等待者能并发运行而不是串行化)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public Task<Releaser> ReaderLockAsync() { lock (m_waitingWriters) { if (m_status >= 0 && m_waitingWriters.Count == 0) { ++m_status; return m_readerReleaser; } else { ++m_readersWaiting; return m_waitingReader.Task.ContinueWith(t => t.Result); } } } |
WriterLockAsync()用于写操作获取锁。就像ReaderLockAsync()方法一样,需要对m_waitingWriters加独占锁,并且要考虑两种情况:写操作要么立即被响应,要么被迫等待。只有当前锁是空闲状态写操作才能立即被响应,因为写操作不能和任意读操作或其他写操作同时运行。所以,如果m_status值为0,我们更改m_status为-1表示现在正在运行一个写操作,并且返回为写操作缓存的Queue<TaskCompletionSource<Releaser>>队列。否则,我们为写操作创建一个新的TaskCompletionSource<Releaser>实例插入到队列中并且返回此实例对应的Task。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public Task<Releaser> WriterLockAsync() { lock (m_waitingWriters) { if (m_status == 0) { m_status = -1; return m_writerReleaser; } else { var waiter = new TaskCompletionSource<Releaser>(); m_waitingWriters.Enqueue(waiter); return waiter.Task; } } } |
现在我们来构建唤醒功能,这些功能在被激活的读操作或写操作完成工作并且想释放它们持有的锁时被调用。ReaderRelease()需要递减读操作激活数计数,并且检查当前锁的状态。如果此时唤醒的是最后一个激活的读操作并且存在写操作正在等待,则会唤醒一个写操作并且设置m_status值为-1标识写操作获得锁。我们不需要检查任何等待的读操作,因为写操作比任何数量的读操作优先得到处理。如果此时没有任何等待的写操作,则任一后续请求的读操作将立即被响应。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | private void ReaderRelease() { TaskCompletionSource<Releaser> toWake = null ; lock (m_waitingWriters) { --m_status; if (m_status == 0 && m_waitingWriters.Count > 0) { m_status = -1; toWake = m_waitingWriters.Dequeue(); } } if (toWake != null ) toWake.SetResult( new Releaser( this , true )); } |
最后,我们构建WriterRelease()方法。当读操作完成时,如果等待队列中存在等待的写操作,我们从队列中取出一个写操作并且完成任务(因为将激活一个新的写操作,旧的写操作完成后,新的写操作会取代它的位置,所以不需要更新锁的状态m_status)。如果等待队列中不存在等待的写操作,但是此时存在等待的读操作,我们会唤醒所有读操作对应的任务,在这种情况下,我们还要为后续的读操作创建一个新的等待任务,并且需要更新m_status为当前激活的读操作数。如果没有任何读操作或写操作,则我们重置锁状态m_status。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | private void WriterRelease() { TaskCompletionSource<Releaser> toWake = null ; bool toWakeIsWriter = false ; lock (m_waitingWriters) { if (m_waitingWriters.Count > 0) { toWake = m_waitingWriters.Dequeue(); toWakeIsWriter = true ; } else if (m_readersWaiting > 0) { toWake = m_waitingReader; m_status = m_readersWaiting; m_readersWaiting = 0; m_waitingReader = new TaskCompletionSource<Releaser>(); } else m_status = 0; } if (toWake != null ) toWake.SetResult( new Releaser( this , toWakeIsWriter)); } |
这就是本节要讲的AsyncReaderWriterLock。
完整源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | /// <summary> /// Async版的 read/writer 锁 /// </summary> public class AsyncReaderWriterLock { // 为了提高性能,我为读/写操作的等待各缓存了一个Task<Releaser> private readonly Task<Releaser> m_readerReleaser; private readonly Task<Releaser> m_writerReleaser; // 写操作的等待队列 private readonly Queue<TaskCompletionSource<Releaser>> m_waitingWriters = new Queue<TaskCompletionSource<Releaser>>(); // 读操作的等待任务 private TaskCompletionSource<Releaser> m_waitingReader = new TaskCompletionSource<Releaser>(); // 当前有多少个等待着的读操作计数 private int m_readersWaiting; // 维护锁的当前状态。0表示锁空闲;-1表示一个写操作获取锁;正值表示一个或多个读操作获取锁。 private int m_status; public AsyncReaderWriterLock() { m_readerReleaser = Task.FromResult( new Releaser( this , false )); m_writerReleaser = Task.FromResult( new Releaser( this , true )); } /// <summary> /// 读操作获取锁 /// </summary> public Task<Releaser> ReaderLockAsync() { lock (m_waitingWriters) { if (m_status >= 0 && m_waitingWriters.Count == 0) { ++m_status; return m_readerReleaser; } else { // 存在等待的写操作或正在执行写操作 ++m_readersWaiting; return m_waitingReader.Task.ContinueWith(t => t.Result); } } } /// <summary> /// 释放读操作 /// </summary> private void ReaderRelease() { TaskCompletionSource<Releaser> toWake = null ; lock (m_waitingWriters) { --m_status; if (m_status == 0 && m_waitingWriters.Count > 0) { // 唤醒的是最后一个激活的读操作并且存在写操作正在等待 m_status = -1; toWake = m_waitingWriters.Dequeue(); } } if (toWake != null ) toWake.SetResult( new Releaser( this , true )); } /// <summary> /// 写操作获取锁 /// </summary> public Task<Releaser> WriterLockAsync() { lock (m_waitingWriters) { if (m_status == 0) { m_status = -1; return m_writerReleaser; } else { // 新的写操作被迫等待 var waiter = new TaskCompletionSource<Releaser>(); m_waitingWriters.Enqueue(waiter); return waiter.Task; } } } /// <summary> /// 释放写操作 /// </summary> private void WriterRelease() { TaskCompletionSource<Releaser> toWake = null ; bool toWakeIsWriter = false ; lock (m_waitingWriters) { if (m_waitingWriters.Count > 0) { toWake = m_waitingWriters.Dequeue(); toWakeIsWriter = true ; } else if (m_readersWaiting > 0) { toWake = m_waitingReader; m_status = m_readersWaiting; m_readersWaiting = 0; m_waitingReader = new TaskCompletionSource<Releaser>(); } else m_status = 0; } if (toWake != null ) toWake.SetResult( new Releaser( this , toWakeIsWriter)); } public struct Releaser : IDisposable { private readonly AsyncReaderWriterLock m_toRelease; // 变量标识是否是写操作,因为我们需要使用不同的方式来释放两种操作的锁 private readonly bool m_writer; internal Releaser(AsyncReaderWriterLock toRelease, bool writer) { m_toRelease = toRelease; m_writer = writer; } public void Dispose() { if (m_toRelease != null ) { if (m_writer) m_toRelease.WriterRelease(); else m_toRelease.ReaderRelease(); } } } } /// <summary> /// 示例:AsyncReaderWriterLock /// </summary> public class AsyncReaderWriterLockTest { public static async void Test() { AsyncReaderWriterLock m_lock = new AsyncReaderWriterLock(); using ( var releaser = await m_lock.WriterLockAsync()) { } using ( var releaser = await m_lock.ReaderLockAsync()) { } } } |
推荐阅读:
构建Async版本的同步基元系列已经全部翻译完,我希望你喜欢它。感谢大家的观看……赞的话请多帮推荐(*^_^*)
原文:《Building Async Coordination Primitives, Part 7: AsyncReaderWriterLock》
作者:滴答的雨
出处:http://www.cnblogs.com/heyuquan/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
欢迎园友讨论下自己的见解,及向我推荐更好的资料。
本文如对您有帮助,还请多帮 【推荐】 下此文。
谢谢!!! (*^_^*)
技术群:(339322839广西IT技术交流),欢迎你的加入
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· SQL Server 2025 AI相关能力初探
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库