16

I see many methods across new framework that uses new asynchronous pattern/language support for async/await in C#. Why is there no Monitor.EnterAsync() or other async lock mechanism that releases current thread & returns as soon as lock is available?

I assume that this is not possible - question is why?

René Vogt
  • 43,056
  • 14
  • 77
  • 99
pg0xC
  • 1,226
  • 10
  • 20
  • I know this question about monitors, but there are some synchronization primitives which do offer async operations: https://msdn.microsoft.com/en-us/library/hh462723.aspx – Dark Falcon Mar 15 '16 at 18:20
  • It's absolutely possible! It's just hard to write (and harder to use correctly), and it was not retrofitted into the existing `Monitor` class because `Monitor` is a rather fundamental type to begin with, and this is a rather sophisticated case (assuming you really, really do need it). See https://github.com/StephenCleary/AsyncEx/wiki/AsyncMonitor for an implementation. – Jeroen Mostert Mar 15 '16 at 18:41
  • 2
    It's possible to implement but things go wrong when you try and actually use it. http://stackoverflow.com/questions/7612602/why-cant-i-use-the-await-operator-within-the-body-of-a-lock-statement – vcsjones Mar 15 '16 at 18:41
  • 2
    Other big problem: the whole `Monitor.Enter` is built to be the fastest locking primitive. `Task` are quite full of overhead... The two wouldn't mix very well. – xanatos Mar 15 '16 at 18:42
  • 1
    There is such a method, on the kind of synchronization object that does not care about which thread owns the lock. Ignoring that detail can get you to step into big doggiedoo with async/await. Use SemaphoreSlim.WaitAsync() – Hans Passant Mar 15 '16 at 19:23

5 Answers5

8

While there is no asynchronous monitor in .NET by default, Stephen Cleary has a great library AsyncEx which deals with synchronization issues when using async/await.

It has an AsyncMonitor class, which does pretty much exactly what you're looking for. You can get it either from GitHub or as a NuGet package.

Usage example:

var monitor = new AsyncMonitor();
using (await monitor.EnterAsync())
{
    // Critical section
}
Gediminas Masaitis
  • 3,172
  • 14
  • 35
7

I assume that this is not possible - question is why?

It's possible, it just hasn't been done yet.

Currently, the only async-compatible synchronization primitive in the BCL is SemaphoreSlim, which can act as a semaphore or a simple mutual-exclusion lock.

I have a basic AsyncMonitor that I wrote, loosely based on Stephen Toub's blog post series. Note that the semantics are slightly different than the BCL Monitor; in particular, it does not permit recursive locks (for reasons I describe on my blog).

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    `System.Threading.Monitor.Enter` takes an `object` as an argument and is `static`. This is useful from a limited resources perspective. `AsyncMonitor` does not have this capability and so I assume you must instantiate one `AsyncMonitor` per object that you wish to lock on. Is it safe to use thousands of these? Tens of thousands? – Little Endian May 20 '20 at 20:18
  • @LittleEndian: `Monitor.Enter` is odd, because in .NET *every* `object` is a monitor. That's a very strange design decision, but it is what it is. Most people follow a convention of defining an explicit `object` for locking, both for code clarity and correctness (some older types will `lock(this)`), so the general usage of `Monitor` is to allocate a separate `object` to be used with the monitor. So you define one `AsyncMonitor` to cover each group of objects you want to lock together. It's safe to use tens of thousands, but I would highly question any design that required that many. – Stephen Cleary May 20 '20 at 20:22
  • "I would highly question any design that required that many." I'm surprised by that opinion. I would think that multi-threading and fine-grained locking would go hand in hand. If I'm processing a collection of ten-thousand objects, it is natural to lock on either the entire collection or the individual items. – Little Endian May 22 '20 at 20:58
  • @LittleEndian: If you have locking on the entry and exit of the queue, why would each object need its own lock? – Stephen Cleary May 22 '20 at 21:02
5

I guess the problem is that by calling Monitor.Enter the current thread wants to gain the lock for the passed object. So you should ask yourself how you would implement a Monitor.EnterAsync? First naive attempt would be:

public async Task EnterAsync(object o)
{
    await Task.Run(() => Monitor.Enter(o));
}

But that obviously does not do what you expect, because the lock would be gained by the thread started for that new Task and not by the calling thread.
You would now need a mechanism to ensure that you can gain the lock after the await. But I currently can't think of a way how to ensure that this will work and that no other thread will gain the lock in between.


These are just my 2 cents (would have posted as comment if it wasn't too long). I'm looking forward to a more enlighting answer for you from someone with more detailed knowledge.

René Vogt
  • 43,056
  • 14
  • 77
  • 99
5

Some synchronization primitives that .Net supplies are managed wrappers around the underlying native objects.

Currently, there are no native synchronization primitives that implement asynchronous locking. So the .Net implementers have to implement that from scratch, which is not so simple as it seems.

Also, the Windows kernel does not provide any feature of "locking-delegation", meaning you can't lock a lock in one thread, and pass the ownership to another thread, that makes the job of implementing such locks extremely difficult.

In my opinion, the third reason is more philosophical one - if you don't want to block - use non - blocking techniques, like using asynchronous IO, lock free algorithms and data structures. If the bottleneck of your application is heavy contention and the locking overhead around it, you can re-design your application in different form without having to need asynchronous locks.

Peter O.
  • 32,158
  • 14
  • 82
  • 96
David Haim
  • 25,446
  • 3
  • 44
  • 78
  • *Monitor is a wrapper around CRITICAL_SECTION* is not true. Can you add any supporting authoritative resource? – Sriram Sakthivel Mar 15 '16 at 19:40
  • Concurrent programming on windows book by Joe duffy calls this out **Physically, the monitor does not include a Windows CRITICAL_SECTION, but it behaves much as though it does.** Pgno: 272 – Sriram Sakthivel Mar 15 '16 at 20:01
2

This worked well for me as detailed here: SemaphoreSlim Class


Semaphores are of two types: local semaphores and named system semaphores.

The former is local to an app. The latter is visible throughout the operating system and is suitable for inter-process synchronization.

The SemaphoreSlim is a lightweight alternative to the Semaphore class that doesn't use Windows kernel semaphores. Unlike the Semaphore class, the SemaphoreSlim class doesn't support named system semaphores.

You can use it as a local semaphore only. The SemaphoreSlim class is the recommended semaphore for synchronization within a single app.

public class ResourceLocker
{
   private Dictionary<string, SemaphoreSlim> _lockers = null;

   private object lockObj = new object();

   public ResourceLocker()
   {
      _lockers = new Dictionary<string, SemaphoreSlim>();
   }

   public SemaphoreSlim GetOrCreateLocker(string resource)
   {
       lock (lockObj)
       {
          if (!_lockers.ContainsKey(resource))
          {
             _lockers.Add(resource, new SemaphoreSlim(1, 1));
          }

             return _lockers?[resource];
        }
    }

    public bool ReleaseLocker(string resource)
    {
       lock (lockObj)
       {
         if (_lockers.ContainsKey(resource))
         {
           var locker = _lockers?[resource];

           if (locker != null)
           {
             locker.Release();

             return true;
            }

             _lockers.Remove(resource);
          }
          return false;
        }//lock
      }
 }

Usage

var resource = "customResource";
var someObject = new SomeObject();
SomeResponse response = null;
var resourceLocker = new ResourceLocker();
try
  {
    var semaSlim = resourceLocker.GetOrCreateLocker(resource);

    semaSlim.Wait();     

    response = someObject.DoSomething();
   }
   finally
   {
     resourceLocker.ReleaseLocker(resource);
   }     

Async

   Task.Run(async ()=>{
        var semaSlim = resourceLocker.GetOrCreateLocker(resource);

        await semaSlim.WaitAsync();     

        response = someObject.DoSomething();

        resourceLocker.ReleaseLocker(resource);
    });
ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
dynamiclynk
  • 2,275
  • 27
  • 31