Something which you shouldn’t be doing very often (or ever) is placing locks around chunks of asynchronous code. You’re almost always slowing down your code, and there has to be a better way to do it. Anyway, if you do try it, C# will give you a compile error along the lines of: ‘cannot await in the body of a lock statement’. This all makes sense, because locks are tied to a thread. If you are using the parallel task library, there is really no guarantee that your code is going to pick back up on the same thread where it originally left off. You can run into problems like this if you try to use a class like “ReadWriteLockSlim”, which doesn’t give compile warning about awaiting within the critical section, but will happily deadlock your code. So here is a solution to all your fears. Luckily SemaphoreSlim has async support.
This is a helper class which just makes a generic disposable for some coding sugar:
raw
    /// <summary>
    /// A class which can be disposed.
    /// </summary>
    public class Disposable : IDisposable
    {
        /// <summary>
        /// The action to perform on dispose.
        /// </summary>
        private readonly Action onDispose;

        /// <summary>
        /// Initializes a new dispoable.
        /// </summary>
        /// <param name="onDispose">The action to perform.</param>
        private Disposable(Action onDispose)
        {
            this.onDispose = onDispose;
        }

        /// <summary>
        /// Creates a disposable class.
        /// </summary>
        /// <param name="onDispose">The dispose action.</param>
        /// <returns>A disposable.</returns>
        public static IDisposable Create(Action onDispose)
        {
            return new Disposable(onDispose);
        }

        /// <summary>
        /// Dispose.
        /// </summary>
        public void Dispose()
        {
            onDispose();
        }
    }
                            
                        
This is a dummy object which needs a lock:
raw
public class LockableObject
    {
        private readonly SemaphoreSlim asyncLock = new SemaphoreSlim(1);

        /// <summary>
        /// Gets a lock on this object.
        /// </summary>
        /// <param name="token">The token.</param>
        /// <returns>A disposable which release the lock.</returns>
        public Task<IDisposable> GetLock(CancellationToken token)
        {
            return this.asyncLock.WaitAsync(token).ContinueWith(result =>
            {
                if (result.IsCompleted && !result.IsFaulted && !result.IsFaulted)
                {
                    return Disposable.Create(() => asyncLock.Release());
                }

                throw result.Exception?.InnerExceptions.FirstOrDefault() ?? result.Exception ??
                      new Exception("Failed to aquire the lock for an unknown reason.");
            });
        }
    }
                            
                        
Here is a little demo test program:
raw
class Program
    {
        static void Main(string[] args)
        {
            // This wont work...
            ////var obj = new object();
            ////
            ////lock (obj)
            ////{
            ////    await TestLock();
            ////}

            TestLock().GetAwaiter().GetResult();
        }

        /// <summary>
        /// Test to see if this lock works
        /// </summary>
        /// <returns>An async task.</returns>
        public static async Task TestLock()
        {
            const int number = 10000;
            var someObject = new LockableObject();

            var parallelSum = 0L;

            using (var barrier = new ManualResetEventSlim(false))
            {

                var tasks = Enumerable.Range(0, number).Select(i => Task.Run(async () =>
                {
                    // Queue up everything behind this barrier so things try to do this all at once.
                    barrier.Wait();

                    using (await someObject.GetLock(CancellationToken.None)) // Comment out this line to make it fail
                    {
                        // Critical Section
                        var t = parallelSum;
                        parallelSum = t + 1;
                    }
                }));

                barrier.Set();

                await Task.WhenAll(tasks);

            }

            if (parallelSum != number)
            {
                throw new Exception($"The counter is wrong. Expected {number}, but got {parallelSum}");
            }
        }
    }
                            
                        
A major caveat here is that this lock is not at all recursive/re-entrant. If you try to call it within the same call stack, you will most certainly deadlock. Other than that, happy locking.