.NET Fiddle

CSharp · Console

/Program.cs

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
					
class Program
{
	public static int CurrentThreadId => Thread.CurrentThread.ManagedThreadId;
	
	AsyncLock asyncLock = new AsyncLock();
	
	public static async Task Main(string []args){
		await new Program().MainAsync();
	}
	
	public async Task MainAsync(){
		await using(await asyncLock.LockAsync()){
			Console.WriteLine($"1. {nameof(MainAsync)} starting on thread {CurrentThreadId}");
			
			var childTask = ChildAsync(delay: TimeSpan.FromMilliseconds(100));			
			await Task.Delay(TimeSpan.FromMilliseconds(101)).ConfigureAwait(false);
			
			Console.WriteLine($"4. {nameof(MainAsync)} continuing on thread {CurrentThreadId}");  
			await using(await asyncLock.LockAsync()){
				Console.WriteLine($"In Critical section on main thread {CurrentThreadId}");
			}
			Console.WriteLine($"6. {nameof(MainAsync)} finished on main thread");
			
			await childTask;
		}
	}
	
	async Task ChildAsync(TimeSpan delay)  
	{
		Console.WriteLine($"2. {nameof(ChildAsync)} starting on thread {CurrentThreadId}");  
  
		await Task.Delay(delay).ConfigureAwait(false);  

		await using (await asyncLock.LockAsync())  
		{  
			Console.WriteLine($"3. {nameof(ChildAsync)} continuing on thread {CurrentThreadId}");  
			Console.WriteLine($"In Critical section on child thread {CurrentThreadId}");
			Console.WriteLine($"5. {nameof(ChildAsync)} finished on child thread");  
		}
	}
}

class AsyncLock : IAsyncDisposable {
	private object locker = new object();
	private string path = "";
	private AsyncLocal<string> contextPath;
	private Random random = new Random();
	private List<TaskCompletionSource> waiters = new List<TaskCompletionSource>();
	
	public AsyncLock(){
		contextPath = new AsyncLocal<string>();
		contextPath.Value = "";
	}
	
	public Task<IAsyncDisposable> LockAsync(){
		bool taken = false;
		TaskCompletionSource waiter = null;
		
		while(!taken){
			lock(locker){
				char nextCharacter = (char)((int)'a' + random.Next('z' - 'a'));
				if(contextPath.Value == path){
					path = path + nextCharacter;
					contextPath.Value = path;
					taken = true;
				}else{
					waiter = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
					waiters.Add(waiter);
				}
			}
			
			if(!taken && waiter != null){
				return waiter.Task.ContinueWith(t => LockAsync()).Result;
			}
		}
		
		return Task.FromResult((IAsyncDisposable)this);
	}
	
	public ValueTask DisposeAsync (){
		lock(locker){
			if(path.Length == 1){
				path = "";
			}else{
				path = path.Substring(0, path.Length - 1);
			}
			
			contextPath.Value = path;
			
			foreach(var waiter in waiters){
				waiter.SetResult();
			}
			waiters.Clear();
			Monitor.PulseAll(locker);
		}
		
		return ValueTask.CompletedTask;
	}
}
;