SynchronizationLockException on Monitor.Exit ao usar o aguardar

Estou criando um código que obtém uma página da Web de um sistema legado que temos. Para evitar consultas excessivas, estou armazenando em cache o URL obtido. Estou usando Monitor.Enter , Monitor.Exit e verificação dupla para evitar que a solicitação seja emitida duas vezes, mas ao liberar o bloqueio com Monitor.Exit , estou recebendo esta exceção:

 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.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.d__15.MoveNext() in c:\Users\me\Documents\Visual Studio 2013\Projects\MyApp\MyApp\Data\ExProvider.cs:line 71 InnerException: 

A linha 56 é o Monitor.Exit . Este é o código que realiza a operação:

 private async Task 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); } } } 

Então, qual é o problema com await e Monitor ? É porque não é o mesmo segmento quando Monitor.Enter que quando Monitor.Exit ?

Você não pode await uma tarefa dentro de um escopo de lock (que é o açúcar sintático para Monitor.Enter e Monitor.Exit ). Usar um Monitor diretamente enganará o compilador, mas não o framework.

async-await não tem afinidade de thread como um Monitor faz. O código após a await provavelmente será executado em um segmento diferente do código antes dele. O que significa que o segmento que libera o Monitor não é necessariamente aquele que o adquiriu.

Ou não use async-await neste caso, ou use uma construção de synchronization diferente como SemaphoreSlim ou um AsyncLock você pode construir sozinho. Aqui está o meu: https://stackoverflow.com/a/21011273/885318

No SendRequest, no entanto, preciso aguardar e, portanto, não consigo usar o bloqueio por algum motivo que não dei muita atenção, portanto, a solução para sincronizar é usar o Monitor.

Deveria ter pensado mais nisso. 🙂

Existem dois problemas com o uso de bloqueios de bloqueio com código async .

O primeiro problema é que – no caso geral – um método async pode retomar a execução em um thread diferente. A maioria dos bloqueios de bloqueio é thread-affine, o que significa que eles devem ser liberados do encadeamento que os possui (o mesmo encadeamento que adquiriu o bloqueio). É essa violação da afinidade de thread do Monitor que causa o SynchronizationLockException . Esse problema não acontece se o await capturar um contexto de execução (por exemplo, um contexto da interface do usuário) e usado para retomar o método async (por exemplo, no thread da interface do usuário). Ou se você apenas teve sorte e o método async foi retomado no mesmo thread do pool de threads.

No entanto, mesmo se você evitar o primeiro problema, ainda terá um segundo problema: qualquer código arbitrário pode ser executado enquanto um método async é “pausado” em um ponto de await . Isto é uma violação de uma regra cardinal de bloqueio (“não execute código arbitrário enquanto segura um bloqueio”). Por exemplo, os bloqueios de afinamento de thread (incluindo Monitor ) geralmente são reentrantes, portanto, mesmo no cenário de thread de interface do usuário, quando o método async é “pausado” (e mantendo o bloqueio), outros methods em execução no thread de interface do usuário podem levar o Bloquear sem problemas.

No Windows Phone 8, use SemaphoreSlim . Este é um tipo que permite tanto o bloqueio quanto a coordenação assíncrona. Use Wait por um bloqueio de bloqueio e WaitAsync por um bloqueio asynchronous.

Você pode usar a class interlocked para simular a instrução de bloqueio, aqui está o código:

  private async Task OpenReport(String report) { var file = _directory.GetFiles(report + ".html"); if (file != null && file.Any()) return file[0].OpenRead(); else { object locker = _locker; try { while (locker == null || Interlocked.CompareExchange(ref _locker, null, locker) != locker) { await Task.Delay(1); locker = _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 { _locker = locker; } } }