20

I am creating a piece of code that gets a webpage from a legacy system we have. In order to avoid excessive querying, I am caching the obtained URL. I am using Monitor.Enter, Monitor.Exit and double checking to avoid that request is issued twice, but when releasing the lock with Monitor.Exit, I am getting this exception:

System.Threading.SynchronizationLockException was caught
  HResult=-2146233064
  Message=Object synchronization method was called from an unsynchronized block of code.
  Source=MyApp
  StackTrace:
       at MyApp.Data.ExProvider.<OpenFeature>d__0.MoveNext() in c:\Users\me\Documents\Visual Studio 2013\Projects\MyApp\MyApp\Data\ExProvider.cs:line 56
    --- End of stack trace from previous location where exception was thrown ---
       at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
       at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
       at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
       at MyApp.Data.ExProvider.<GetSupportFor>d__15.MoveNext() in c:\Users\me\Documents\Visual Studio 2013\Projects\MyApp\MyApp\Data\ExProvider.cs:line 71
  InnerException: 

The line 56 is the Monitor.Exit. This is the code that performs the operation:

private async Task<Stream> OpenReport(String report)
{
    var file = _directory.GetFiles(report+ ".html");
    if (file != null && file.Any())
        return file[0].OpenRead();
    else
    {
        try
        {
            Monitor.Enter(_locker);
            FileInfo newFile = new FileInfo(Path.Combine(_directory.FullName, report + ".html"));
            if (!newFile.Exists) // Double check
            {
                using (var target = newFile.OpenWrite())
                {
                    WebRequest request = WebRequest.Create(BuildUrl(report));
                    var response = await request.GetResponseAsync();
                    using (var source = response.GetResponseStream())
                        source.CopyTo(target);
                }
            }
            return newFile.OpenRead();
        }
        finally
        {
            Monitor.Exit(_locker);
        }
    }
}

So what is the problem with await and Monitor? Is it because it is not the same thread when Monitor.Enter than when Monitor.Exit?

svick
  • 236,525
  • 50
  • 385
  • 514
vtortola
  • 34,709
  • 29
  • 161
  • 263

2 Answers2

47

You can't await a task inside a lock scope (which is syntactic sugar for Monitor.Enter and Monitor.Exit). Using a Monitor directly will fool the compiler but not the framework.

async-await has no thread-affinity like a Monitor does. The code after the await will probably run in a different thread than the code before it. Which means that the thread that releases the Monitor isn't necessarily the one that acquired it.

Either don't use async-await in this case, or use a different synchronization construct like SemaphoreSlim or an AsyncLock you can build yourself. Here's mine: https://stackoverflow.com/a/21011273/885318

Community
  • 1
  • 1
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • Should it be ok with a ManualResetEventSlim? – vtortola Jan 28 '14 at 11:27
  • 2
    It can be, but it's much easier with a SemaphoreSlim set to 1, because you can await it and it isn't thread-affine – i3arnon Jan 28 '14 at 11:29
  • Thanks for the explanation. It makes a lot of sense. – vtortola Jan 28 '14 at 15:24
  • I'm not getting any exception in following code block when I was expecting SynchronizationLockException.Not sure why it is not throwing exception as per known behavior of await and lock.`static void Main(string[] args) { AsyncFunctionWithLock(); } private static async void AsyncFunctionWithLock() { Monitor.Enter(lockObject); { await AsyncFunctionWithLock2(); } Monitor.Exit(lockObject); } private static async Task AsyncFunctionWithLock2() { }` – RBT Mar 12 '16 at 01:39
  • 1
    @RasikBihariTiwari `AsyncFunctionWithLock2` isn't actually asynchronous. It doesn't await anything so it continues on synchronously on the same thread. – i3arnon Mar 12 '16 at 01:46
  • @i3arnon appreciate ur quick response.I was finally able to get that exception with this change in my function:`private static async Task AsyncFunctionWithLock3() { //simulate long running taks so task scheduler schedules it //on a threadpool thread instead of the main UI thread itself. string filename = @"C:\temp.txt"; byte[] result; using (FileStream SourceStream = File.Open(filename, FileMode.Open)) { result = new byte[SourceStream.Length]; await SourceStream.ReadAsync(result, 0, (int)SourceStream.Length); }}}` – RBT Mar 12 '16 at 02:32
10

In SendRequest however, I need to 'await', and thus I'm unable to use lock for some reason I didn't give much thought, so the solution to synchronize is to use Monitor.

Should have given it more thought. :)

There are two problems with using blocking locks with async code.

The first problem is that - in the general case - an async method may resume executing on a different thread. Most blocking locks are thread-affine, meaning that they must be released from the thread that owns them (the same thread that acquired the lock). It is this violation of Monitor thread-affinity that causes the SynchronizationLockException. This problem does not happen if the await captures an execution context (e.g., a UI context) and used that to resume the async method (e.g., on the UI thread). Or if you just got lucky and the async method happened to resume on the same thread pool thread.

However, even if you avoid the first problem, you still have a second problem: any arbitrary code can execute while an async method is "paused" at an await point. This is a violation of a cardinal rule of locking ("do not execute arbitrary code while holding a lock"). For example, thread-affine locks (including Monitor) are generally re-entrant, so even in the UI thread scenario, when your async method is "paused" (and holding the lock), other methods running on the UI thread can take the lock without any problems.

On Windows Phone 8, use SemaphoreSlim instead. This is a type that allows both blocking and asynchronous coordination. Use Wait for a blocking lock and WaitAsync for an asynchronous lock.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks. SemaphoreSlim looks like what I need. – warwolf Jan 27 '14 at 06:57
  • FYI: answer merged from http://stackoverflow.com/questions/21368917/c-sharp-synchronizationlockexception-on-monitor-exit-when-async-method-called-fr – Shog9 Jul 25 '14 at 20:00
  • @Stephen Cleary, I have purchased your book on concurrency and is still on my "to-read" list. Does your book cover the above scenarios as well? – wenn32 Dec 09 '22 at 04:12
  • @wenn32: I do cover asynchronous locks but I don't remember if I go into detail with the regular lock in asynchronous code or not. – Stephen Cleary Dec 09 '22 at 05:31
  • @Stephen Cleary, maybe for the third edition ;) Anyways, thanks a lot for the great book. Hoping to get some time to finish it soon. Excited to read. Take care. – wenn32 Dec 09 '22 at 06:48