├── .gitattributes ├── .gitignore ├── AsyncLock.sln ├── AsyncLock ├── AsyncLock.cs ├── AsyncLock.csproj ├── AsyncLock.snk └── NullDisposable.cs ├── LICENSE ├── README.md ├── UnitTests ├── AsyncIdTests.cs ├── AsyncSpawn.cs ├── CancellationTests.cs ├── LimitedResource.cs ├── MixedSyncAsync.cs ├── MixedSyncAsyncTimed.cs ├── ParallelExecutionTests.cs ├── ReentracePermittedTests.cs ├── ReentranceLockoutTests.cs ├── TaskWaiter.cs ├── TryLockTests.cs ├── TryLockTestsAsync.cs ├── TryLockTestsAsyncOut.cs └── UnitTests.csproj └── publish.fish /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.sln text eol=crlf 4 | *.vcxproj text eol=crlf 5 | *.filters text eol=crlf 6 | *.cpp diff=csharp eol=crlf 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | -------------------------------------------------------------------------------- /AsyncLock.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29521.150 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncLock", "AsyncLock\AsyncLock.csproj", "{077768A9-D1A4-48BB-8ECF-C66D50E47396}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{7864530D-D038-495F-9283-34A185FBC20F}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {077768A9-D1A4-48BB-8ECF-C66D50E47396}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {077768A9-D1A4-48BB-8ECF-C66D50E47396}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {077768A9-D1A4-48BB-8ECF-C66D50E47396}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {077768A9-D1A4-48BB-8ECF-C66D50E47396}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {7864530D-D038-495F-9283-34A185FBC20F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {7864530D-D038-495F-9283-34A185FBC20F}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {7864530D-D038-495F-9283-34A185FBC20F}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {7864530D-D038-495F-9283-34A185FBC20F}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {EE50F46F-060F-49C5-9FD9-2304E0A6C2C6} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /AsyncLock/AsyncLock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.ExceptionServices; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace NeoSmart.AsyncLock 8 | { 9 | public class AsyncLock 10 | { 11 | private SemaphoreSlim _reentrancy = new SemaphoreSlim(1, 1); 12 | private int _reentrances = 0; 13 | // We are using this SemaphoreSlim like a posix condition variable. 14 | // We only want to wake waiters, one or more of whom will try to obtain 15 | // a different lock to do their thing. So long as we can guarantee no 16 | // wakes are missed, the number of awakees is not important. 17 | // Ideally, this would be "friend" for access only from InnerLock, but 18 | // whatever. 19 | internal SemaphoreSlim _retry = new SemaphoreSlim(0, 1); 20 | private const long UnlockedId = 0x00; // "owning" task id when unlocked 21 | internal long _owningId = UnlockedId; 22 | internal int _owningThreadId = (int) UnlockedId; 23 | private static long AsyncStackCounter = 0; 24 | // An AsyncLocal is not really the task-based equivalent to a ThreadLocal, in that 25 | // it does not track the async flow (as the documentation describes) but rather it is 26 | // associated with a stack snapshot. Mutation of the AsyncLocal in an await call does 27 | // not change the value observed by the parent when the call returns, so if you want to 28 | // use it as a persistent async flow identifier, the value needs to be set at the outer- 29 | // most level and never touched internally. 30 | private static readonly AsyncLocal _asyncId = new AsyncLocal(); 31 | private static long AsyncId => _asyncId.Value; 32 | 33 | #if NETSTANDARD1_3 34 | private static int ThreadCounter = 0x00; 35 | private static ThreadLocal LocalThreadId = new ThreadLocal(() => ++ThreadCounter); 36 | private static int ThreadId => LocalThreadId.Value; 37 | #else 38 | private static int ThreadId => Thread.CurrentThread.ManagedThreadId; 39 | #endif 40 | 41 | public AsyncLock() 42 | { 43 | } 44 | 45 | #if !DEBUG 46 | readonly 47 | #endif 48 | struct InnerLock : IDisposable 49 | { 50 | private readonly AsyncLock _parent; 51 | private readonly long _oldId; 52 | private readonly int _oldThreadId; 53 | #if DEBUG 54 | private bool _disposed; 55 | #endif 56 | 57 | internal InnerLock(AsyncLock parent, long oldId, int oldThreadId) 58 | { 59 | _parent = parent; 60 | _oldId = oldId; 61 | _oldThreadId = oldThreadId; 62 | #if DEBUG 63 | _disposed = false; 64 | #endif 65 | } 66 | 67 | internal async Task ObtainLockAsync(CancellationToken cancellationToken = default) 68 | { 69 | while (true) 70 | { 71 | await _parent._reentrancy.WaitAsync(cancellationToken).ConfigureAwait(false); 72 | if (InnerTryEnter(synchronous: false)) 73 | { 74 | break; 75 | } 76 | // We need to wait for someone to leave the lock before trying again. 77 | // We need to "atomically" obtain _retry and release _reentrancy, but there 78 | // is no equivalent to a condition variable. Instead, we call *but don't await* 79 | // _retry.WaitAsync(), then release the reentrancy lock, *then* await the saved task. 80 | var waitTask = _parent._retry.WaitAsync(cancellationToken).ConfigureAwait(false); 81 | _parent._reentrancy.Release(); 82 | await waitTask; 83 | } 84 | // Reset the owning thread id after all await calls have finished, otherwise we 85 | // could be resumed on a different thread and set an incorrect value. 86 | _parent._owningThreadId = ThreadId; 87 | _parent._reentrancy.Release(); 88 | return this; 89 | } 90 | 91 | internal async Task TryObtainLockAsync(TimeSpan timeout) 92 | { 93 | // In case of zero-timeout, don't even wait for protective lock contention 94 | if (timeout == TimeSpan.Zero) 95 | { 96 | _parent._reentrancy.Wait(timeout); 97 | if (InnerTryEnter(synchronous: false)) 98 | { 99 | // Reset the owning thread id after all await calls have finished, otherwise we 100 | // could be resumed on a different thread and set an incorrect value. 101 | _parent._owningThreadId = ThreadId; 102 | _parent._reentrancy.Release(); 103 | return this; 104 | } 105 | _parent._reentrancy.Release(); 106 | return null; 107 | } 108 | 109 | var now = DateTimeOffset.UtcNow; 110 | var last = now; 111 | var remainder = timeout; 112 | 113 | // We need to wait for someone to leave the lock before trying again. 114 | while (remainder > TimeSpan.Zero) 115 | { 116 | await _parent._reentrancy.WaitAsync(remainder).ConfigureAwait(false); 117 | if (InnerTryEnter(synchronous: false)) 118 | { 119 | // Reset the owning thread id after all await calls have finished, otherwise we 120 | // could be resumed on a different thread and set an incorrect value. 121 | _parent._owningThreadId = ThreadId; 122 | _parent._reentrancy.Release(); 123 | return this; 124 | } 125 | _parent._reentrancy.Release(); 126 | 127 | now = DateTimeOffset.UtcNow; 128 | remainder -= now - last; 129 | last = now; 130 | if (remainder < TimeSpan.Zero) 131 | { 132 | _parent._reentrancy.Release(); 133 | return null; 134 | } 135 | 136 | var waitTask = _parent._retry.WaitAsync(remainder).ConfigureAwait(false); 137 | _parent._reentrancy.Release(); 138 | if (!await waitTask) 139 | { 140 | return null; 141 | } 142 | 143 | now = DateTimeOffset.UtcNow; 144 | remainder -= now - last; 145 | last = now; 146 | } 147 | 148 | return null; 149 | } 150 | 151 | internal async Task TryObtainLockAsync(CancellationToken cancellationToken = default) 152 | { 153 | try 154 | { 155 | while (true) 156 | { 157 | await _parent._reentrancy.WaitAsync(cancellationToken).ConfigureAwait(false); 158 | if (InnerTryEnter(synchronous: false)) 159 | { 160 | break; 161 | } 162 | // We need to wait for someone to leave the lock before trying again. 163 | var waitTask = _parent._retry.WaitAsync(cancellationToken).ConfigureAwait(false); 164 | _parent._reentrancy.Release(); 165 | await waitTask; 166 | } 167 | } 168 | catch (OperationCanceledException) 169 | { 170 | return null; 171 | } 172 | 173 | // Reset the owning thread id after all await calls have finished, otherwise we 174 | // could be resumed on a different thread and set an incorrect value. 175 | _parent._owningThreadId = ThreadId; 176 | _parent._reentrancy.Release(); 177 | return this; 178 | } 179 | 180 | internal IDisposable ObtainLock(CancellationToken cancellationToken = default) 181 | { 182 | while (true) 183 | { 184 | _parent._reentrancy.Wait(cancellationToken); 185 | if (InnerTryEnter(synchronous: true)) 186 | { 187 | _parent._reentrancy.Release(); 188 | break; 189 | } 190 | // We need to wait for someone to leave the lock before trying again. 191 | var waitTask = _parent._retry.WaitAsync(cancellationToken); 192 | _parent._reentrancy.Release(); 193 | // This should be safe since the task we are awaiting doesn't need to make progress 194 | // itself to complete - it will be completed by another thread altogether. cf SemaphoreSlim internals. 195 | waitTask.GetAwaiter().GetResult(); 196 | } 197 | return this; 198 | } 199 | 200 | internal IDisposable? TryObtainLock(TimeSpan timeout) 201 | { 202 | // In case of zero-timeout, don't even wait for protective lock contention 203 | if (timeout == TimeSpan.Zero) 204 | { 205 | _parent._reentrancy.Wait(timeout); 206 | if (InnerTryEnter(synchronous: true)) 207 | { 208 | _parent._reentrancy.Release(); 209 | return this; 210 | } 211 | _parent._reentrancy.Release(); 212 | return null; 213 | } 214 | 215 | var now = DateTimeOffset.UtcNow; 216 | var last = now; 217 | var remainder = timeout; 218 | 219 | // We need to wait for someone to leave the lock before trying again. 220 | while (remainder > TimeSpan.Zero) 221 | { 222 | _parent._reentrancy.Wait(remainder); 223 | if (InnerTryEnter(synchronous: true)) 224 | { 225 | _parent._reentrancy.Release(); 226 | return this; 227 | } 228 | 229 | now = DateTimeOffset.UtcNow; 230 | remainder -= now - last; 231 | last = now; 232 | 233 | var waitTask = _parent._retry.WaitAsync(remainder); 234 | _parent._reentrancy.Release(); 235 | if (!waitTask.GetAwaiter().GetResult()) 236 | { 237 | return null; 238 | } 239 | 240 | now = DateTimeOffset.UtcNow; 241 | remainder -= now - last; 242 | last = now; 243 | } 244 | 245 | return null; 246 | } 247 | 248 | private bool InnerTryEnter(bool synchronous = false) 249 | { 250 | bool result = false; 251 | if (synchronous) 252 | { 253 | if (_parent._owningThreadId == UnlockedId) 254 | { 255 | _parent._owningThreadId = ThreadId; 256 | } 257 | else if (_parent._owningThreadId != ThreadId) 258 | { 259 | return false; 260 | } 261 | _parent._owningId = AsyncLock.AsyncId; 262 | } 263 | else 264 | { 265 | if (_parent._owningId == UnlockedId) 266 | { 267 | _parent._owningId = AsyncLock.AsyncId; 268 | } 269 | else if (_parent._owningId != _oldId) 270 | { 271 | // Another thread currently owns the lock 272 | return false; 273 | } 274 | else 275 | { 276 | // Nested re-entrance 277 | _parent._owningId = AsyncId; 278 | } 279 | } 280 | 281 | // We can go in 282 | _parent._reentrances += 1; 283 | result = true; 284 | return result; 285 | } 286 | 287 | public void Dispose() 288 | { 289 | #if DEBUG 290 | Debug.Assert(!_disposed); 291 | _disposed = true; 292 | #endif 293 | var @this = this; 294 | var oldId = this._oldId; 295 | var oldThreadId = this._oldThreadId; 296 | @this._parent._reentrancy.Wait(); 297 | try 298 | { 299 | @this._parent._reentrances -= 1; 300 | @this._parent._owningId = oldId; 301 | @this._parent._owningThreadId = oldThreadId; 302 | if (@this._parent._reentrances == 0) 303 | { 304 | // The owning thread is always the same so long as we 305 | // are in a nested stack call. We reset the owning id 306 | // only when the lock is fully unlocked. 307 | @this._parent._owningId = UnlockedId; 308 | @this._parent._owningThreadId = (int)UnlockedId; 309 | } 310 | // We can't place this within the _reentrances == 0 block above because we might 311 | // still need to notify a parallel reentrant task to wake. I think. 312 | // This should not be a race condition since we only wait on _retry with _reentrancy locked, 313 | // then release _reentrancy so the Dispose() call can obtain it to signal _retry in a big hack. 314 | if (@this._parent._retry.CurrentCount == 0) 315 | { 316 | @this._parent._retry.Release(); 317 | } 318 | } 319 | finally 320 | { 321 | @this._parent._reentrancy.Release(); 322 | } 323 | } 324 | } 325 | 326 | // Make sure InnerLock.LockAsync() does not use await, because an async function triggers a snapshot of 327 | // the AsyncLocal value. 328 | public Task LockAsync(CancellationToken cancellationToken = default) 329 | { 330 | var @lock = new InnerLock(this, _asyncId.Value, ThreadId); 331 | _asyncId.Value = Interlocked.Increment(ref AsyncLock.AsyncStackCounter); 332 | return @lock.ObtainLockAsync(cancellationToken); 333 | } 334 | 335 | // Make sure InnerLock.LockAsync() does not use await, because an async function triggers a snapshot of 336 | // the AsyncLocal value. 337 | public Task TryLockAsync(Action callback, TimeSpan timeout) 338 | { 339 | var @lock = new InnerLock(this, _asyncId.Value, ThreadId); 340 | _asyncId.Value = Interlocked.Increment(ref AsyncLock.AsyncStackCounter); 341 | 342 | return @lock.TryObtainLockAsync(timeout) 343 | .ContinueWith(state => 344 | { 345 | if (state.Exception is AggregateException ex) 346 | { 347 | ExceptionDispatchInfo.Capture(ex.InnerException!).Throw(); 348 | } 349 | var disposableLock = state.Result; 350 | if (disposableLock is null) 351 | { 352 | return false; 353 | } 354 | 355 | try 356 | { 357 | callback(); 358 | } 359 | finally 360 | { 361 | disposableLock.Dispose(); 362 | } 363 | return true; 364 | }); 365 | } 366 | 367 | // Make sure InnerLock.LockAsync() does not use await, because an async function triggers a snapshot of 368 | // the AsyncLocal value. 369 | public Task TryLockAsync(Func callback, TimeSpan timeout) 370 | { 371 | var @lock = new InnerLock(this, _asyncId.Value, ThreadId); 372 | _asyncId.Value = Interlocked.Increment(ref AsyncLock.AsyncStackCounter); 373 | 374 | return @lock.TryObtainLockAsync(timeout) 375 | .ContinueWith(state => 376 | { 377 | if (state.Exception is AggregateException ex) 378 | { 379 | ExceptionDispatchInfo.Capture(ex.InnerException!).Throw(); 380 | } 381 | var disposableLock = state.Result; 382 | if (disposableLock is null) 383 | { 384 | return Task.FromResult(false); 385 | } 386 | 387 | return callback() 388 | .ContinueWith(result => 389 | { 390 | disposableLock.Dispose(); 391 | 392 | if (result.Exception is AggregateException ex) 393 | { 394 | ExceptionDispatchInfo.Capture(ex.InnerException!).Throw(); 395 | } 396 | 397 | return true; 398 | }, TaskScheduler.Default); 399 | }, TaskScheduler.Default).Unwrap(); 400 | } 401 | 402 | // Make sure InnerLock.TryLockAsync() does not use await, because an async function triggers a snapshot of 403 | // the AsyncLocal value. 404 | public Task TryLockAsync(Action callback, CancellationToken cancellationToken) 405 | { 406 | var @lock = new InnerLock(this, _asyncId.Value, ThreadId); 407 | _asyncId.Value = Interlocked.Increment(ref AsyncLock.AsyncStackCounter); 408 | 409 | return @lock.TryObtainLockAsync(cancellationToken) 410 | .ContinueWith(state => 411 | { 412 | if (state.Exception is AggregateException ex) 413 | { 414 | ExceptionDispatchInfo.Capture(ex.InnerException!).Throw(); 415 | } 416 | var disposableLock = state.Result; 417 | if (disposableLock is null) 418 | { 419 | return false; 420 | } 421 | 422 | try 423 | { 424 | callback(); 425 | } 426 | finally 427 | { 428 | disposableLock.Dispose(); 429 | } 430 | return true; 431 | }, TaskScheduler.Default); 432 | } 433 | 434 | // Make sure InnerLock.LockAsync() does not use await, because an async function triggers a snapshot of 435 | // the AsyncLocal value. 436 | public Task TryLockAsync(Func callback, CancellationToken cancellationToken) 437 | { 438 | var @lock = new InnerLock(this, _asyncId.Value, ThreadId); 439 | _asyncId.Value = Interlocked.Increment(ref AsyncLock.AsyncStackCounter); 440 | 441 | return @lock.TryObtainLockAsync(cancellationToken) 442 | .ContinueWith(state => 443 | { 444 | if (state.Exception is AggregateException ex) 445 | { 446 | ExceptionDispatchInfo.Capture(ex.InnerException!).Throw(); 447 | } 448 | var disposableLock = state.Result; 449 | if (disposableLock is null) 450 | { 451 | return Task.FromResult(false); 452 | } 453 | 454 | return callback() 455 | .ContinueWith(result => 456 | { 457 | disposableLock.Dispose(); 458 | 459 | if (result.Exception is AggregateException ex) 460 | { 461 | ExceptionDispatchInfo.Capture(ex.InnerException!).Throw(); 462 | } 463 | 464 | return true; 465 | }, TaskScheduler.Default); 466 | }, TaskScheduler.Default).Unwrap(); 467 | } 468 | 469 | public IDisposable Lock(CancellationToken cancellationToken = default) 470 | { 471 | var @lock = new InnerLock(this, _asyncId.Value, ThreadId); 472 | // Increment the async stack counter to prevent a child task from getting 473 | // the lock at the same time as a child thread. 474 | _asyncId.Value = Interlocked.Increment(ref AsyncLock.AsyncStackCounter); 475 | return @lock.ObtainLock(cancellationToken); 476 | } 477 | 478 | public bool TryLock(Action callback, TimeSpan timeout) 479 | { 480 | var @lock = new InnerLock(this, _asyncId.Value, ThreadId); 481 | // Increment the async stack counter to prevent a child task from getting 482 | // the lock at the same time as a child thread. 483 | _asyncId.Value = Interlocked.Increment(ref AsyncLock.AsyncStackCounter); 484 | var lockDisposable = @lock.TryObtainLock(timeout); 485 | if (lockDisposable is null) 486 | { 487 | return false; 488 | } 489 | 490 | // Execute the callback then release the lock 491 | try 492 | { 493 | callback(); 494 | } 495 | finally 496 | { 497 | lockDisposable.Dispose(); 498 | } 499 | return true; 500 | } 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /AsyncLock/AsyncLock.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard1.3;netstandard2.1 5 | NeoSmart.AsyncLock 6 | NeoSmart.AsyncLock 7 | True 8 | 3.3.0-preview1 9 | NeoSmart Technologies, Mahmoud Al-Qudsi 10 | NeoSmart Technologies 11 | NeoSmart.AsyncLock 12 | A C# lock replacement for async/await, supporting recursion/re-entrance and asynchronous waits. Handles async recursion correctly - note that Nito.AsyncEx does not! 13 | Copyright NeoSmart Technologies 2017-2025 14 | MIT 15 | https://neosmart.net/blog/2017/asynclock-an-asyncawait-friendly-locking-library-for-c-and-net/ 16 | https://github.com/neosmart/AsyncLock 17 | git 18 | asynclock, async await, async, await, lock, synchronization 19 | 20 | 3.2: New TryLock() and TryLockAsync() methods, CancellationToken support for synchronous locking routines. 21 | 22 | 3.0: Smarter method of detecting recursion for faster and more-reliable locking on all platforms. 23 | 24 | 3.1: Added synchronous locking that may be intermixed with async locking. 25 | 26 | 3.2: Added TryLock() and TryLockAsync() methods. 27 | MIT 28 | enable 29 | preview 30 | 31 | 32 | 33 | true 34 | True 35 | AsyncLock.snk 36 | README.md 37 | 38 | 39 | 40 | 41 | True 42 | \ 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /AsyncLock/AsyncLock.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neosmart/AsyncLock/a3c7bc9c8009fbb5217fbaa322a15bfb212220c7/AsyncLock/AsyncLock.snk -------------------------------------------------------------------------------- /AsyncLock/NullDisposable.cs: -------------------------------------------------------------------------------- 1 | #if TRY_LOCK_OUT_BOOL 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace NeoSmart.AsyncLock 7 | { 8 | sealed class NullDisposable : IDisposable 9 | { 10 | public void Dispose() 11 | { 12 | } 13 | } 14 | } 15 | #endif 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 NeoSmart Technologies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AsyncLock: An async/await-friendly lock 2 | 3 | [![NuGet](https://img.shields.io/nuget/v/NeoSmart.AsyncLock.svg)](https://www.nuget.org/packages/NeoSmart.AsyncLock) 4 | 5 | AsyncLock is an async/await-friendly lock implementation for .NET Standard, making writing code like the snippet below (mostly) possible: 6 | 7 | ```csharp 8 | lock (_lockObject) 9 | { 10 | await DoSomething(); 11 | } 12 | ``` 13 | Unlike most other so-called "async locks" for C#, AsyncLock is actually designed to support the programming paradigm `lock` encourages, not just the technical elements. You can read more about the pitfalls with other so-called asynchronous locks and the difficulties of creating a reentrance-safe implementation [here](https://neosmart.net/blog/2017/asynclock-an-asyncawait-friendly-locking-library-for-c-and-net/). 14 | 15 | With `AsyncLock`, you don't have to worry about which thread is running what code in order to determine whether or not your locks will have any effect or if they'll be bypassed completely, you just write code the way you normally would and you'll find AsyncLock to correctly marshal access to protected code segments. 16 | 17 | ### Using AsyncLock 18 | 19 | There are only three functions to familiarize yourself with: the `AsyncLock()` constructor and the two locking variants `Lock()`/`LockAsync()` . 20 | 21 | `AsyncLock()` creates a new asynchronous lock. A separate AsyncLock should be used for each "critical operation" you will be performing. (Or you can use a global lock just like some people still insist on using global mutexes and semaphores. We won't judge too harshly.) 22 | 23 | Everywhere you would normally use `lock (_lockObject)` you will now use one of 24 | 25 | * `using (_lock.Lock())` or 26 | * `using (await _lock.LockAsync())` 27 | 28 | That's all there is to it! 29 | 30 | ### Async-friendly locking by design 31 | 32 | Much like the`SemaphoreSlim` class, `AsyncLock` offers two different "wait" options, a blocking `Lock()` call and the asynchronous `LockAsync()` call. The utmost scare should be taken to never call `LockAsync()` without an `await` before it, for obvious reasons. 33 | 34 | Upon using `LockAsync()`, `AsyncLock` will attempt to obtain exclusive access to the lock. Should that not be possible in the current state, it will cede its execution slot and return to the caller, allowing the system to marshal resources efficiently as needed without blocking until the lock becomes available. Once the lock is available, the `AsyncLock()` call will resume, transferring execution to the protected section of the code. 35 | 36 | ### AsyncLock usage example 37 | 38 | ```csharp 39 | private class AsyncLockTest 40 | { 41 | var _lock = new AsyncLock(); 42 | 43 | void Test() 44 | { 45 | // The code below will be run immediately (likely in a new thread) 46 | Task.Run(async () => 47 | { 48 | // A first call to LockAsync() will obtain the lock without blocking 49 | using (await _lock.LockAsync()) 50 | { 51 | // A second call to LockAsync() will be recognized as being 52 | // reentrant and permitted to go through without blocking. 53 | using (await _lock.LockAsync()) 54 | { 55 | // We now exclusively hold the lock for 1 minute 56 | await Task.Delay(TimeSpan.FromMinutes(1)); 57 | } 58 | } 59 | }).Wait(TimeSpan.FromSeconds(30)); 60 | 61 | // This call to obtain the lock is made synchronously from the main thread. 62 | // It will, however, block until the asynchronous code which obtained the lock 63 | // above finishes. 64 | using (_lock.Lock()) 65 | { 66 | // Now we have obtained exclusive access. 67 | // 68 | } 69 | } 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /UnitTests/AsyncIdTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using NeoSmart.AsyncLock; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | #if false 11 | namespace AsyncLockTests 12 | { 13 | [TestClass] 14 | public class AsyncIdTests 15 | { 16 | [TestMethod] 17 | public void TaskIdUniqueness() 18 | { 19 | var testCount = 100; 20 | var countdown = new CountdownEvent(testCount); 21 | var failure = new ManualResetEventSlim(false); 22 | var threadIds = new SortedSet(); 23 | var abort = new SemaphoreSlim(0, 1); 24 | 25 | for (int i = 0; i < testCount; ++i) 26 | { 27 | Task.Run(async () => 28 | { 29 | lock (threadIds) 30 | { 31 | if (!threadIds.Add(AsyncLock.ThreadId)) 32 | { 33 | failure.Set(); 34 | } 35 | } 36 | countdown.Signal(); 37 | await abort.WaitAsync(); 38 | }); 39 | } 40 | 41 | if (WaitHandle.WaitAny(new[] { countdown.WaitHandle, failure.WaitHandle }) == 1) 42 | { 43 | Assert.Fail("A duplicate thread id was found!"); 44 | } 45 | 46 | abort.Release(); 47 | } 48 | 49 | public void ThreadIdUniqueness() 50 | { 51 | var testCount = 100; 52 | var countdown = new CountdownEvent(testCount); 53 | var failure = new ManualResetEventSlim(false); 54 | var threadIds = new SortedSet(); 55 | var abort = new SemaphoreSlim(0, 1); 56 | 57 | for (int i = 0; i < testCount; ++i) 58 | { 59 | Task.Run(async () => 60 | { 61 | lock (threadIds) 62 | { 63 | if (!threadIds.Add(AsyncLock.ThreadId)) 64 | { 65 | failure.Set(); 66 | } 67 | } 68 | countdown.Signal(); 69 | await abort.WaitAsync(); 70 | }); 71 | } 72 | 73 | if (WaitHandle.WaitAny(new[] { countdown.WaitHandle, failure.WaitHandle }) == 1) 74 | { 75 | Assert.Fail("A duplicate thread id was found!"); 76 | } 77 | 78 | abort.Release(); 79 | } 80 | } 81 | } 82 | #endif 83 | -------------------------------------------------------------------------------- /UnitTests/AsyncSpawn.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using NeoSmart.AsyncLock; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace AsyncLockTests 10 | { 11 | /// 12 | /// Creates multiple independent tasks, each with its own lock, and runs them 13 | /// all in parallel. There should be no contention for the lock between the 14 | /// parallelly executed tasks, but each task then recursively obtains what 15 | /// should be the same lock - which should again be contention-free - after 16 | /// an await point that may or may not resume on the same actual thread the 17 | /// previous lock was obtained with. 18 | /// 19 | [TestClass] 20 | public class AsyncSpawn 21 | { 22 | public readonly struct NullDisposable : IDisposable 23 | { 24 | public void Dispose() { } 25 | } 26 | 27 | public async Task AsyncExecution(bool locked) 28 | { 29 | var count = 0; 30 | var tasks = new List(70); 31 | var asyncLock = new AsyncLock(); 32 | var rng = new Random(); 33 | 34 | { 35 | using var l = locked ? await asyncLock.LockAsync() : new NullDisposable(); 36 | 37 | for (int i = 0; i < 10; ++i) 38 | { 39 | var task = Task.Run(async () => 40 | { 41 | using (await asyncLock.LockAsync()) 42 | { 43 | Assert.AreEqual(Interlocked.Increment(ref count), 1); 44 | await Task.Yield(); 45 | Assert.AreEqual(count, 1); 46 | await Task.Delay(rng.Next(1, 10) * 10); 47 | using (await asyncLock.LockAsync()) 48 | { 49 | await Task.Delay(rng.Next(1, 10) * 10); 50 | Assert.AreEqual(Interlocked.Decrement(ref count), 0); 51 | } 52 | 53 | Assert.AreEqual(count, 0); 54 | } 55 | 56 | }); 57 | tasks.Add(task); 58 | } 59 | } 60 | 61 | await Task.WhenAll(tasks); 62 | 63 | Assert.AreEqual(count, 0); 64 | } 65 | 66 | [TestMethod] 67 | public async Task AsyncExecutionLocked() 68 | { 69 | await AsyncExecution(true); 70 | } 71 | 72 | [TestMethod] 73 | public async Task AsyncExecutionUnlocked() 74 | { 75 | await AsyncExecution(false); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /UnitTests/CancellationTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.VisualStudio.TestTools.UnitTesting; 8 | using NeoSmart.AsyncLock; 9 | 10 | namespace AsyncLockTests 11 | { 12 | [TestClass] 13 | public class CancellationTests 14 | { 15 | [TestMethod] 16 | public void CancellingWait() 17 | { 18 | var @lock = new AsyncLock(); 19 | var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); 20 | Task.Run(async () => 21 | { 22 | await @lock.LockAsync(cts.Token); 23 | }).Wait(); 24 | Assert.ThrowsExceptionAsync(async () => 25 | { 26 | using (await @lock.LockAsync(cts.Token)) 27 | Assert.Fail("should never reach here if cancellation works properly"); 28 | }).Wait(); 29 | 30 | } 31 | 32 | [TestMethod] 33 | public void CancellingWaitSync() 34 | { 35 | var asyncLock = new AsyncLock(); 36 | var cts = new CancellationTokenSource(250); 37 | var delayStarted = new ManualResetEventSlim(false); 38 | var waiter1Finished = new SemaphoreSlim(0, 1); 39 | 40 | new Thread(() => 41 | { 42 | using (asyncLock.Lock(cts.Token)) 43 | { 44 | // hold the lock until our later attempt is called 45 | delayStarted.Set(); 46 | waiter1Finished.Wait(); 47 | } 48 | }).Start(); 49 | 50 | Assert.ThrowsException(() => 51 | { 52 | delayStarted.Wait(); 53 | using (asyncLock.Lock(cts.Token)) 54 | { 55 | Assert.Fail("should never reach here if cancellation works properly."); 56 | } 57 | }); 58 | waiter1Finished.Release(1); 59 | 60 | // We should still be able to obtain a lock afterward to make sure resources were reobtained 61 | var newCts = new CancellationTokenSource(2000); 62 | using (asyncLock.Lock(newCts.Token)) 63 | { 64 | // reaching this line means the test passed 65 | // a OperationCanceledException will indicate test failure 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /UnitTests/LimitedResource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace AsyncLockTests 9 | { 10 | /// 11 | /// A fake resource that will invoke a callback if more than n instances are simultaneously accessed 12 | /// 13 | class LimitedResource 14 | { 15 | private readonly int _max = 1; 16 | private int _unsafe = 0; 17 | private readonly Action _failureCallback; 18 | 19 | public LimitedResource(Action onFailure, int maxSimultaneous = 1) 20 | { 21 | _max = maxSimultaneous; 22 | _failureCallback = onFailure; 23 | } 24 | 25 | public void BeginSomethingDangerous() 26 | { 27 | if (Interlocked.Increment(ref _unsafe) > _max) 28 | { 29 | _failureCallback(); 30 | } 31 | } 32 | 33 | public void EndSomethingDangerous() 34 | { 35 | Interlocked.Decrement(ref _unsafe); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /UnitTests/MixedSyncAsync.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using NeoSmart.AsyncLock; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace AsyncLockTests 10 | { 11 | /// 12 | /// Creates multiple indepndent tasks, each with its own lock, and runs them 13 | /// all in parallel. There should be no contention for the lock between the 14 | /// parallelly executed tasks, but each task then recursively obtains what 15 | /// should be the same lock - which should again be contention-free - after 16 | /// an await point that may or may not resume on the same actual thread the 17 | /// previous lock was obtained with. 18 | /// 19 | [TestClass] 20 | public class MixedSyncAsync 21 | { 22 | [TestMethod] 23 | public async Task MixedSyncAsyncExecution() 24 | { 25 | var count = 0; 26 | var threads = new List(10); 27 | var tasks = new List(10); 28 | var asyncLock = new AsyncLock(); 29 | var rng = new Random(); 30 | 31 | { 32 | using var l = asyncLock.Lock(); 33 | for (int i = 0; i < 10; ++i) 34 | { 35 | var thread = new Thread(() => 36 | { 37 | using (asyncLock.Lock()) 38 | { 39 | Assert.AreEqual(Interlocked.Increment(ref count), 1); 40 | Thread.Sleep(rng.Next(1, 10) * 10); 41 | using (asyncLock.Lock()) 42 | { 43 | Thread.Sleep(10); 44 | Assert.AreEqual(Interlocked.Decrement(ref count), 0); 45 | } 46 | 47 | Assert.AreEqual(count, 0); 48 | } 49 | 50 | }); 51 | thread.Start(); 52 | threads.Add(thread); 53 | } 54 | 55 | for (int i = 0; i < 10; ++i) 56 | { 57 | var task = Task.Run(async () => 58 | { 59 | using (await asyncLock.LockAsync()) 60 | { 61 | Assert.AreEqual(Interlocked.Increment(ref count), 1); 62 | Assert.AreEqual(count, 1); 63 | await Task.Delay(rng.Next(1, 10) * 10); 64 | using (await asyncLock.LockAsync()) 65 | { 66 | await Task.Delay(10); 67 | Assert.AreEqual(Interlocked.Decrement(ref count), 0); 68 | } 69 | 70 | Assert.AreEqual(count, 0); 71 | } 72 | 73 | }); 74 | tasks.Add(task); 75 | } 76 | } 77 | 78 | await Task.WhenAll(tasks); 79 | foreach (var thread in threads) 80 | { 81 | thread.Join(); 82 | } 83 | 84 | Assert.AreEqual(count, 0); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /UnitTests/MixedSyncAsyncTimed.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using NeoSmart.AsyncLock; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace AsyncLockTests 10 | { 11 | /// 12 | /// Creates multiple indepndent tasks, each with its own lock, and runs them 13 | /// all in parallel. There should be no contention for the lock between the 14 | /// parallelly executed tasks, but each task then recursively obtains what 15 | /// should be the same lock - which should again be contention-free - after 16 | /// an await point that may or may not resume on the same actual thread the 17 | /// previous lock was obtained with. 18 | /// 19 | [TestClass] 20 | public class MixedSyncAsyncTimed 21 | { 22 | [TestMethod] 23 | public async Task MixedSyncAsyncExecution() 24 | { 25 | var count = 0; 26 | var threads = new List(10); 27 | var tasks = new List(10); 28 | var asyncLock = new AsyncLock(); 29 | var rng = new Random(); 30 | 31 | { 32 | using var l = asyncLock.Lock(); 33 | for (int i = 0; i < 10; ++i) 34 | { 35 | var thread = new Thread(() => 36 | { 37 | using (asyncLock.Lock()) 38 | { 39 | Assert.AreEqual(Interlocked.Increment(ref count), 1); 40 | Thread.Sleep(rng.Next(1, 10) * 10); 41 | using (asyncLock.Lock()) 42 | { 43 | Thread.Sleep(10); 44 | Assert.AreEqual(Interlocked.Decrement(ref count), 0); 45 | } 46 | 47 | Assert.AreEqual(count, 0); 48 | } 49 | 50 | }); 51 | thread.Start(); 52 | threads.Add(thread); 53 | } 54 | 55 | for (int i = 0; i < 10; ++i) 56 | { 57 | var captured = i; 58 | var task = Task.Run(async () => 59 | { 60 | using (await asyncLock.LockAsync()) 61 | { 62 | Assert.AreEqual(Interlocked.Increment(ref count), 1); 63 | Assert.AreEqual(count, 1); 64 | await Task.Delay(rng.Next(1, 10) * 10); 65 | if (captured % 2 == 0) 66 | { 67 | using (await asyncLock.LockAsync()) 68 | { 69 | await Task.Yield(); 70 | Assert.AreEqual(Interlocked.Decrement(ref count), 0); 71 | } 72 | } 73 | else 74 | { 75 | var executed = await asyncLock.TryLockAsync(async () => 76 | { 77 | // Throw in a recursive async lock invocation 78 | bool nestedExecuted = await asyncLock.TryLockAsync(async () => 79 | { 80 | await Task.Yield(); 81 | Interlocked.Increment(ref count); 82 | }, TimeSpan.FromMilliseconds(1 /* guarantees no zero-ms optimizations */)); 83 | Assert.IsTrue(nestedExecuted); 84 | Interlocked.Decrement(ref count); 85 | await Task.Yield(); 86 | Assert.AreEqual(Interlocked.Decrement(ref count), 0); 87 | }, TimeSpan.FromMilliseconds(rng.Next(1, 10) * 10)); 88 | Assert.IsTrue(executed, "TryLockAsync() did not end up executing!"); 89 | } 90 | 91 | Assert.AreEqual(count, 0); 92 | } 93 | 94 | }); 95 | tasks.Add(task); 96 | } 97 | } 98 | 99 | await Task.WhenAll(tasks); 100 | foreach (var thread in threads) 101 | { 102 | thread.Join(); 103 | } 104 | 105 | Assert.AreEqual(count, 0); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /UnitTests/ParallelExecutionTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using NeoSmart.AsyncLock; 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace AsyncLockTests 8 | { 9 | /// 10 | /// Creates multiple indepndent tasks, each with its own lock, and runs them 11 | /// all in parallel. There should be no contention for the lock between the 12 | /// parallelly executed tasks, but each task then recursively obtains what 13 | /// should be the same lock - which should again be contention-free - after 14 | /// an await point that may or may not resume on the same actual thread the 15 | /// previous lock was obtained with. 16 | /// 17 | [TestClass] 18 | public class ParallelExecutionTests 19 | { 20 | [TestMethod] 21 | public async Task ParallelExecution() 22 | { 23 | await Task.WhenAll(Enumerable.Range(0, 1).Select(SomeMethod)); 24 | } 25 | 26 | private static async Task SomeMethod(int i) 27 | { 28 | var asyncLock = new AsyncLock(); 29 | System.Diagnostics.Debug.WriteLine($"Outside {i}"); 30 | await Task.Delay(100); 31 | using (await asyncLock.LockAsync()) 32 | { 33 | System.Diagnostics.Debug.WriteLine($"Lock1 {i}"); 34 | await Task.Delay(100); 35 | using (await asyncLock.LockAsync()) 36 | { 37 | System.Diagnostics.Debug.WriteLine($"Lock2 {i}"); 38 | await Task.Delay(100); 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /UnitTests/ReentracePermittedTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Threading.Tasks; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using NeoSmart.AsyncLock; 5 | 6 | namespace AsyncLockTests 7 | { 8 | [TestClass] 9 | public class ReentracePermittedTests 10 | { 11 | readonly AsyncLock _lock = new AsyncLock(); 12 | 13 | [TestMethod] 14 | public async Task NestedCallReentrance() 15 | { 16 | using (await _lock.LockAsync()) 17 | using (await _lock.LockAsync()) 18 | { 19 | Debug.WriteLine("Hello from NestedCallReentrance!"); 20 | } 21 | } 22 | 23 | [TestMethod] 24 | public void NestedAsyncCallReentrance() 25 | { 26 | var task = Task.Run(async () => 27 | { 28 | using (await _lock.LockAsync()) 29 | using (await _lock.LockAsync()) 30 | { 31 | Debug.WriteLine("Hello from NestedCallReentrance!"); 32 | } 33 | }); 34 | 35 | new TaskWaiter(task).WaitOne(); 36 | } 37 | 38 | private async Task NestedFunctionAsync() 39 | { 40 | using (await _lock.LockAsync()) 41 | { 42 | Debug.WriteLine("Hello from another (nested) function!"); 43 | } 44 | } 45 | 46 | [TestMethod] 47 | public async Task NestedFunctionCallReentrance() 48 | { 49 | using (await _lock.LockAsync()) 50 | { 51 | await NestedFunctionAsync(); 52 | } 53 | } 54 | 55 | // Issue #18 56 | [TestMethod] 57 | //[Timeout(5)] 58 | public async Task BackToBackReentrance() 59 | { 60 | var asyncLock = new AsyncLock(); 61 | async Task InnerFunctionAsync() 62 | { 63 | using (await asyncLock.LockAsync()) 64 | { 65 | // 66 | } 67 | } 68 | using (await asyncLock.LockAsync()) 69 | { 70 | await InnerFunctionAsync(); 71 | await InnerFunctionAsync(); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /UnitTests/ReentranceLockoutTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | using NeoSmart.AsyncLock; 8 | 9 | namespace AsyncLockTests 10 | { 11 | [TestClass] 12 | public class ReentranceLockoutTests 13 | { 14 | private AsyncLock _lock; 15 | private LimitedResource _resource; 16 | private CountdownEvent _countdown; 17 | private Random _random = new Random((int)DateTime.UtcNow.Ticks); 18 | private int DelayInterval => _random.Next(1, 5) * 10; 19 | 20 | private void ResourceSimulation(Action action) 21 | { 22 | _lock = new AsyncLock(); 23 | // Start n threads and have them obtain the lock and randomly wait, then verify 24 | var failure = new ManualResetEventSlim(false); 25 | _resource = new LimitedResource(() => 26 | { 27 | failure.Set(); 28 | }); 29 | 30 | var testCount = 20; 31 | _countdown = new CountdownEvent(testCount); 32 | 33 | for (int i = 0; i < testCount; ++i) 34 | { 35 | action(); 36 | } 37 | 38 | if (WaitHandle.WaitAny(new[] { _countdown.WaitHandle, failure.WaitHandle }) == 1) 39 | { 40 | Assert.Fail("More than one thread simultaneously accessed the underlying resource!"); 41 | } 42 | } 43 | 44 | private async void ThreadEntryPoint() 45 | { 46 | using (await _lock.LockAsync()) 47 | { 48 | _resource.BeginSomethingDangerous(); 49 | Thread.Sleep(DelayInterval); 50 | _resource.EndSomethingDangerous(); 51 | } 52 | _countdown.Signal(); 53 | } 54 | 55 | /// 56 | /// Tests whether the lock successfully prevents multiple threads from obtaining a lock simultaneously when sharing a function entrypoint. 57 | /// 58 | [TestMethod] 59 | public void MultipleThreadsMethodLockout() 60 | { 61 | ResourceSimulation(() => 62 | { 63 | var t = new Thread(ThreadEntryPoint); 64 | t.Start(); 65 | }); 66 | } 67 | 68 | /// 69 | /// Tests whether the lock successfully prevents multiple threads from obtaining a lock simultaneously when sharing nothing. 70 | /// 71 | [TestMethod] 72 | public void MultipleThreadsLockout() 73 | { 74 | ResourceSimulation(() => 75 | { 76 | var t = new Thread(async () => 77 | { 78 | using (await _lock.LockAsync()) 79 | { 80 | _resource.BeginSomethingDangerous(); 81 | Thread.Sleep(DelayInterval); 82 | _resource.EndSomethingDangerous(); 83 | } 84 | _countdown.Signal(); 85 | }); 86 | t.Start(); 87 | }); 88 | } 89 | 90 | /// 91 | /// Tests whether the lock successfully prevents multiple threads from obtaining a lock simultaneously when sharing a local ThreadStart 92 | /// 93 | [TestMethod] 94 | public void MultipleThreadsThreadStartLockout() 95 | { 96 | ThreadStart work = async () => 97 | { 98 | using (await _lock.LockAsync()) 99 | { 100 | _resource.BeginSomethingDangerous(); 101 | Thread.Sleep(DelayInterval); 102 | _resource.EndSomethingDangerous(); 103 | } 104 | _countdown.Signal(); 105 | }; 106 | 107 | ResourceSimulation(() => 108 | { 109 | var t = new Thread(work); 110 | t.Start(); 111 | }); 112 | } 113 | 114 | [TestMethod] 115 | public void AsyncLockout() 116 | { 117 | ResourceSimulation(() => 118 | { 119 | Task.Run(async () => 120 | { 121 | using (await _lock.LockAsync()) 122 | { 123 | _resource.BeginSomethingDangerous(); 124 | Thread.Sleep(DelayInterval); 125 | _resource.EndSomethingDangerous(); 126 | } 127 | _countdown.Signal(); 128 | }); 129 | }); 130 | } 131 | 132 | [TestMethod] 133 | public void AsyncDelayLockout() 134 | { 135 | ResourceSimulation(() => 136 | { 137 | Task.Run(async () => 138 | { 139 | using (await _lock.LockAsync()) 140 | { 141 | _resource.BeginSomethingDangerous(); 142 | await Task.Delay(DelayInterval); 143 | _resource.EndSomethingDangerous(); 144 | } 145 | _countdown.Signal(); 146 | }); 147 | }); 148 | } 149 | 150 | [TestMethod] 151 | public async Task NestedAsyncLockout() 152 | { 153 | var taskStarted = new SemaphoreSlim(0, 1); 154 | var taskEnded = new SemaphoreSlim(0, 1); 155 | var @lock = new AsyncLock(); 156 | using (await @lock.LockAsync()) 157 | { 158 | var task = Task.Run(async () => 159 | { 160 | taskStarted.Release(); 161 | using (await @lock.LockAsync()) 162 | { 163 | Debug.WriteLine("Hello from within an async task!"); 164 | } 165 | await taskEnded.WaitAsync(); 166 | }); 167 | 168 | taskStarted.Wait(); 169 | Assert.IsFalse(new TaskWaiter(task).WaitOne(100)); 170 | taskEnded.Release(); 171 | } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /UnitTests/TaskWaiter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace AsyncLockTests 9 | { 10 | /// 11 | /// A guaranteed-safe method of "synchronously" waiting on tasks to finish. Cannot be used on .NET Core 12 | /// 13 | class TaskWaiter : EventWaitHandle 14 | { 15 | public TaskWaiter(Task task) 16 | : base(false, EventResetMode.ManualReset) 17 | { 18 | new Thread(async () => 19 | { 20 | await task; 21 | Set(); 22 | }).Start(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /UnitTests/TryLockTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using NeoSmart.AsyncLock; 3 | using System; 4 | using System.Threading; 5 | 6 | namespace AsyncLockTests 7 | { 8 | [TestClass] 9 | public class TryLockTests 10 | { 11 | [TestMethod] 12 | public void NoContention() 13 | { 14 | var @lock = new AsyncLock(); 15 | 16 | Assert.IsTrue(@lock.TryLock(() => { }, default)); 17 | } 18 | 19 | [TestMethod] 20 | public void ContentionEarlyReturn() 21 | { 22 | var @lock = new AsyncLock(); 23 | 24 | using (@lock.Lock()) 25 | { 26 | var thread = new Thread(() => 27 | { 28 | Assert.IsFalse(@lock.TryLock(() => throw new Exception("This should never be executed"), default)); 29 | }); 30 | thread.Start(); 31 | thread.Join(); 32 | } 33 | } 34 | 35 | [TestMethod] 36 | public void ContentionDelayedExecution() => ContentionalExecution(50, 250, true); 37 | 38 | [TestMethod] 39 | public void ContentionNoExecution() => ContentionalExecution(250, 50, false); 40 | 41 | [TestMethod] 42 | public void ContentionNoExecutionZeroTimeout() => ContentionalExecution(250, 0, false); 43 | 44 | private void ContentionalExecution(int unlockDelayMs, int lockTimeoutMs, bool expectedResult) 45 | { 46 | int step = 0; 47 | var @lock = new AsyncLock(); 48 | 49 | var locked = @lock.Lock(); 50 | Interlocked.Increment(ref step); 51 | 52 | using var eventTestThreadStarted = new AutoResetEvent(false); 53 | using var eventSleepNotStarted = new AutoResetEvent(false); 54 | using var eventAboutToWait = new AutoResetEvent(false); 55 | 56 | var unlockThread = new Thread(() => 57 | { 58 | eventTestThreadStarted.WaitOne(); 59 | eventSleepNotStarted.Set(); 60 | Thread.Sleep(unlockDelayMs); 61 | eventAboutToWait.WaitOne(); 62 | Interlocked.Increment(ref step); 63 | locked.Dispose(); 64 | }); 65 | unlockThread.Start(); 66 | 67 | var testThread = new Thread(() => 68 | { 69 | eventTestThreadStarted.Set(); 70 | eventSleepNotStarted.WaitOne(); 71 | eventAboutToWait.Set(); 72 | Assert.IsTrue((!expectedResult) ^ @lock.TryLock(() => 73 | { 74 | Assert.AreEqual(2, step); 75 | }, TimeSpan.FromMilliseconds(lockTimeoutMs))); 76 | }); 77 | testThread.Start(); 78 | 79 | unlockThread.Join(); 80 | testThread.Join(); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /UnitTests/TryLockTestsAsync.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using NeoSmart.AsyncLock; 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace AsyncLockTests 8 | { 9 | class LocalException : Exception { 10 | public LocalException(string message) : base(message) { } 11 | } 12 | 13 | [TestClass] 14 | public class TryLockTestsAsync 15 | { 16 | [TestMethod] 17 | public async Task NoContention() 18 | { 19 | var @lock = new AsyncLock(); 20 | 21 | Assert.IsTrue(await @lock.TryLockAsync(() => { }, TimeSpan.Zero)); 22 | } 23 | 24 | /// 25 | /// Assert that exceptions are bubbled up after the lock is disposed 26 | /// 27 | /// 28 | [TestMethod] 29 | public async Task NoContentionThrows() 30 | { 31 | var @lock = new AsyncLock(); 32 | 33 | await Assert.ThrowsExceptionAsync(async () => 34 | { 35 | await @lock.TryLockAsync(async () => { 36 | await Task.Yield(); 37 | throw new LocalException("This exception needs to be bubbled up"); 38 | }, TimeSpan.Zero); 39 | }); 40 | } 41 | 42 | [TestMethod] 43 | public async Task ContentionEarlyReturn() 44 | { 45 | var @lock = new AsyncLock(); 46 | 47 | using (await @lock.LockAsync()) 48 | { 49 | var thread = new Thread(async () => 50 | { 51 | Assert.IsFalse(await @lock.TryLockAsync(() => throw new Exception("This should never be executed"), TimeSpan.Zero)); 52 | }); 53 | thread.Start(); 54 | thread.Join(); 55 | } 56 | } 57 | 58 | [TestMethod] 59 | public async Task ContentionDelayedExecution() => await ContentionalExecution(50, 250, true); 60 | 61 | [TestMethod] 62 | public async Task ContentionNoExecution() => await ContentionalExecution(250, 50, false); 63 | 64 | [TestMethod] 65 | public async Task ContentionNoExecutionZeroTimeout() => await ContentionalExecution(250, 0, false); 66 | 67 | private async Task ContentionalExecution(int unlockDelayMs, int lockTimeoutMs, bool expectedResult) 68 | { 69 | int step = 0; 70 | var @lock = new AsyncLock(); 71 | 72 | var locked = await @lock.LockAsync(); 73 | Interlocked.Increment(ref step); 74 | 75 | using var eventTestThreadStarted = new SemaphoreSlim(0, 1); 76 | using var eventSleepNotStarted = new SemaphoreSlim(0, 1); 77 | using var eventAboutToWait = new SemaphoreSlim(0, 1); 78 | 79 | var unlockThread = new Thread(async () => 80 | { 81 | await eventTestThreadStarted.WaitAsync(); 82 | eventSleepNotStarted.Release(); 83 | Thread.Sleep(unlockDelayMs); 84 | await eventAboutToWait.WaitAsync(); 85 | Interlocked.Increment(ref step); 86 | locked.Dispose(); 87 | }); 88 | unlockThread.Start(); 89 | 90 | var testThread = new Thread(async () => 91 | { 92 | eventTestThreadStarted.Release(); 93 | await eventSleepNotStarted.WaitAsync(); 94 | eventAboutToWait.Release(); 95 | Assert.IsTrue((!expectedResult) ^ await @lock.TryLockAsync(() => 96 | { 97 | Assert.AreEqual(2, step); 98 | }, TimeSpan.FromMilliseconds(lockTimeoutMs))); 99 | }); 100 | testThread.Start(); 101 | 102 | unlockThread.Join(); 103 | testThread.Join(); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /UnitTests/TryLockTestsAsyncOut.cs: -------------------------------------------------------------------------------- 1 | #if TRY_LOCK_OUT_BOOL 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using NeoSmart.AsyncLock; 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace AsyncLockTests 10 | { 11 | [TestClass] 12 | public class TryLockTestsAsyncOut 13 | { 14 | [TestMethod] 15 | public async Task NoContention() 16 | { 17 | var @lock = new AsyncLock(); 18 | 19 | Assert.IsTrue(await @lock.TryLockAsync(() => { }, TimeSpan.Zero)); 20 | } 21 | 22 | /// 23 | /// Assert that exceptions are bubbled up after the lock is disposed 24 | /// 25 | /// 26 | [TestMethod] 27 | public async Task NoContentionThrows() 28 | { 29 | var @lock = new AsyncLock(); 30 | 31 | await Assert.ThrowsExceptionAsync(async () => 32 | { 33 | using (await @lock.TryLockAsync(TimeSpan.Zero, out var locked)) 34 | { 35 | if (locked) 36 | { 37 | await Task.Yield(); 38 | throw new LocalException("This exception needs to be bubbled up"); 39 | } 40 | } 41 | }); 42 | } 43 | 44 | [TestMethod] 45 | public async Task ContentionEarlyReturn() 46 | { 47 | var @lock = new AsyncLock(); 48 | 49 | using (await @lock.LockAsync()) 50 | { 51 | var thread = new Thread(async () => 52 | { 53 | await Task.Yield(); 54 | var disposable = @lock.TryLockAsync(TimeSpan.Zero, out var locked); 55 | Assert.IsFalse(locked); 56 | }); 57 | thread.Start(); 58 | thread.Join(); 59 | } 60 | } 61 | 62 | [TestMethod] 63 | public async Task ContentionDelayedExecution() => await ContentionalExecution(50, 250, true); 64 | 65 | [TestMethod] 66 | public async Task ContentionNoExecution() => await ContentionalExecution(250, 50, false); 67 | 68 | [TestMethod] 69 | public async Task ContentionNoExecutionZeroTimeout() => await ContentionalExecution(250, 0, false); 70 | 71 | private async Task ContentionalExecution(int unlockDelayMs, int lockTimeoutMs, bool expectedResult) 72 | { 73 | int step = 0; 74 | var @lock = new AsyncLock(); 75 | 76 | var locked = await @lock.LockAsync(); 77 | Interlocked.Increment(ref step); 78 | 79 | using var eventTestThreadStarted = new SemaphoreSlim(0, 1); 80 | using var eventSleepNotStarted = new SemaphoreSlim(0, 1); 81 | using var eventAboutToWait = new SemaphoreSlim(0, 1); 82 | 83 | var unlockThread = new Thread(async () => 84 | { 85 | await eventTestThreadStarted.WaitAsync(); 86 | eventSleepNotStarted.Release(); 87 | Thread.Sleep(unlockDelayMs); 88 | await eventAboutToWait.WaitAsync(); 89 | Interlocked.Increment(ref step); 90 | locked.Dispose(); 91 | }); 92 | unlockThread.Start(); 93 | 94 | var testThread = new Thread(async () => 95 | { 96 | eventTestThreadStarted.Release(); 97 | await eventSleepNotStarted.WaitAsync(); 98 | eventAboutToWait.Release(); 99 | 100 | await @lock.TryLockAsync(TimeSpan.FromMilliseconds(lockTimeoutMs), out var locked); 101 | Assert.IsTrue((!expectedResult) ^ locked); 102 | 103 | if (locked) 104 | { 105 | Assert.AreEqual(2, step); 106 | 107 | } 108 | }); 109 | testThread.Start(); 110 | 111 | unlockThread.Join(); 112 | testThread.Join(); 113 | } 114 | } 115 | } 116 | 117 | #endif 118 | -------------------------------------------------------------------------------- /UnitTests/UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | false 7 | 8 | AsyncLockTests 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /publish.fish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env fish 2 | 3 | function csproj_field 4 | set csproj $argv[1] 5 | set field $argv[2] 6 | set value (string replace -rf "\\s*<$field>(.*)" '$1' < $csproj | string trim)[1] 7 | 8 | if ! string match -qr -- '.' $value 9 | echo "Could not extract value of $field from $csproj" 1>&2 10 | exit 1 11 | end 12 | 13 | echo $value 14 | end 15 | 16 | function publish_csproj 17 | set csproj $argv[1] 18 | if ! test -f $csproj 19 | echo "Could not find project file $csproj!" 1>&2 20 | exit 1 21 | end 22 | 23 | set -l pkgname (csproj_field $csproj "PackageId") 24 | set -l pkgversion (csproj_field $csproj "Version") 25 | 26 | if ! dotnet build -c Release -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg $csproj 27 | exit 1 28 | end 29 | 30 | set nupkg (dirname $csproj)/bin/Release/$pkgname.$pkgversion.nupkg 31 | if ! test -f $nupkg 32 | echo "Could not find nuget package $nupkg!" 1>&2 33 | exit 1 34 | end 35 | 36 | set snupkg (dirname $csproj)/bin/Release/$pkgname.$pkgversion.snupkg 37 | if ! test -f $nupkg 38 | echo "Could not find nuget symbol package $snupkg!" 1>&2 39 | exit 1 40 | end 41 | 42 | if ! nuget push $nupkg 43 | exit 1 44 | end 45 | 46 | # This message is printed by nuget when pushing an actual package, but not for the snupkg: 47 | echo "Pushing $snupkg to 'https://nuget.org'" 48 | if ! nuget push $snupkg 49 | echo "Error publishing snupkg" 50 | exit 1 51 | end 52 | echo "Your snupkg package was pushed" 53 | 54 | end 55 | 56 | if string match -qr -- . $argv[1] 57 | set csproj $argv[1] 58 | publish_csproj $csproj 59 | else 60 | publish_csproj ./AsyncLock/*.csproj 61 | end 62 | --------------------------------------------------------------------------------