Por que não posso usar o operador ‘await’ no corpo de uma instrução de bloqueio?

A palavra-chave await em C # (.NET Async CTP) não é permitida a partir de uma instrução de bloqueio.

Do MSDN :

Uma expressão await não pode ser usada em uma function síncrona, em uma expressão de consulta, no bloco catch ou finally de uma instrução de tratamento de exceção, no bloco de uma instrução de bloqueio ou em um contexto inseguro.

Suponho que isso seja difícil ou impossível para a equipe de compiladores implementar por algum motivo.

Eu tentei trabalhar com a declaração using:

class Async { public static async Task Lock(object obj) { while (!Monitor.TryEnter(obj)) await TaskEx.Yield(); return new ExitDisposable(obj); } private class ExitDisposable : IDisposable { private readonly object obj; public ExitDisposable(object obj) { this.obj = obj; } public void Dispose() { Monitor.Exit(this.obj); } } } // example usage using (await Async.Lock(padlock)) { await SomethingAsync(); } 

No entanto, isso não funciona como esperado. A chamada para Monitor.Exit em ExitDisposable.Dispose parece bloquear indefinidamente (na maior parte do tempo) causando deadlocks à medida que outros threads tentam adquirir o bloqueio. Eu suspeito que a falta de confiabilidade do meu trabalho em volta e a razão pela qual esperar declarações não são permitidas na declaração de bloqueio estão de alguma forma relacionadas.

Alguém sabe por que esperar não é permitido dentro do corpo de uma declaração de bloqueio?

Suponho que isso seja difícil ou impossível para a equipe de compiladores implementar por algum motivo.

Não, não é nada difícil ou impossível de implementar – o fato de você ter implementado você mesmo é um testemunho desse fato. Pelo contrário, é uma ideia incrivelmente ruim e, portanto, não permitimos isso, para protegê-lo de cometer esse erro.

chamada para Monitor.Exit em ExitDisposable.Dispose parece bloquear indefinidamente (na maioria das vezes) causando deadlocks como outros segmentos tentam adquirir o bloqueio. Eu suspeito que a falta de confiabilidade do meu trabalho em volta e a razão pela qual esperar declarações não são permitidas na declaração de bloqueio estão de alguma forma relacionadas.

Correto, você descobriu por que o tornamos ilegal. Esperar dentro de uma fechadura é uma receita para produzir deadlocks.

Tenho certeza de que você pode ver o porquê: código arbitrário é executado entre o momento em que o await devolve o controle ao chamador e o método é retomado . Esse código arbitrário pode estar removendo bloqueios que produzem inversões de ordenação de bloqueio e, portanto, bloqueios.

Pior, o código poderia ser retomado em outro thread (em cenários avançados; normalmente você pega novamente no thread que aguarda, mas não necessariamente), nesse caso, o desbloqueio estaria desbloqueando um bloqueio em um thread diferente do thread que levou fora da fechadura. Essa é uma boa ideia? Não.

Observo que também é uma “pior prática” fazer um yield return dentro de uma lock , pelo mesmo motivo. É legal fazê-lo, mas eu gostaria que tivéssemos feito isso ilegal. Não vamos cometer o mesmo erro por “aguardar”.

Use o método SemaphoreSlim.WaitAsync .

  await mySemaphoreSlim.WaitAsync(); try { await Stuff(); } finally { mySemaphoreSlim.Release(); } 

Basicamente, seria a coisa errada a fazer.

Existem duas maneiras de implementar isso:

  • Mantenha o bloqueio, soltando-o apenas no final do bloco .
    Essa é uma idéia muito ruim, já que você não sabe quanto tempo a operação assíncrona vai levar. Você só deve segurar os bloqueios por um período mínimo de tempo. Também é potencialmente impossível, já que um thread possui um bloqueio, não um método – e você pode nem mesmo executar o resto do método asynchronous no mesmo thread (dependendo do agendador de tarefas).

  • Solte a trava na espera e readquira quando a espera retornar
    Isso viola o princípio de menos assombro IMO, em que o método asynchronous deve se comportar o mais próximo possível, como o código síncrono equivalente – a menos que você use Monitor.Wait em um bloco de bloqueio, espera possuir o bloqueio pela duração do bloco.

Então basicamente existem dois requisitos concorrentes aqui – você não deveria estar tentando fazer o primeiro aqui, e se você quiser pegar a segunda abordagem você pode tornar o código muito mais claro tendo dois blocos de bloqueio separados separados pela expressão await:

 // Now it's clear where the locks will be acquired and released lock (foo) { } var result = await something; lock (foo) { } 

Então, ao proibir que você espere no próprio bloco de bloqueio, a linguagem o está forçando a pensar sobre o que você realmente quer fazer, e tornando essa escolha mais clara no código que você escreve.

Isso se refere a http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx , http://winrtstoragehelper.codeplex.com/ , loja de aplicativos do Windows 8 e .net 4.5

Aqui está o meu ângulo sobre isso:

O recurso de linguagem async / await torna muitas coisas bastante fáceis, mas também introduz um cenário que raramente era encontrado antes de ser tão fácil usar chamadas assíncronas: reentrance.

Isso é especialmente verdadeiro para manipuladores de events, porque, para muitos events, você não tem nenhuma pista sobre o que está acontecendo depois de retornar do manipulador de events. Uma coisa que pode realmente acontecer é que o método asynchronous que você está esperando no primeiro manipulador de events, é chamado de outro manipulador de events ainda no mesmo encadeamento.

Aqui está um cenário real que eu encontrei em um aplicativo de loja de aplicativos do windows 8: Meu aplicativo tem dois frameworks: entrando e saindo de um quadro que eu quero carregar / seguro alguns dados para arquivo / armazenamento. Os events OnNavigatedTo / From são usados ​​para salvar e carregar. O salvamento e o carregamento são feitos por alguma function de utilitário asynchronous (como http://winrtstoragehelper.codeplex.com/ ). Ao navegar do quadro 1 para o quadro 2 ou na outra direção, as operações de carregamento asynchronous e seguro são chamadas e aguardadas. Os manipuladores de events se tornam async retornando void => eles não podem ser esperados.

No entanto, a primeira operação de abertura de arquivo (permite dizer: dentro de uma function de salvamento) do utilitário é assíncrona também e assim o primeiro espera retorna o controle para a estrutura, que posteriormente chama o outro utilitário (carga) através do segundo manipulador de events. A carga agora tenta abrir o mesmo arquivo e, se o arquivo estiver aberto para a operação de salvamento, falhará com uma exceção ACCESSDENIED.

Uma solução mínima para mim é proteger o access a arquivos por meio de um uso e um AsyncLock.

 private static readonly AsyncLock m_lock = new AsyncLock(); ... using (await m_lock.LockAsync()) { file = await folder.GetFileAsync(fileName); IRandomAccessStream readStream = await file.OpenAsync(FileAccessMode.Read); using (Stream inStream = Task.Run(() => readStream.AsStreamForRead()).Result) { return (T)serializer.Deserialize(inStream); } } 

Por favor, note que o seu bloqueio basicamente bloqueia toda a operação de arquivo para o utilitário com apenas um bloqueio, que é desnecessariamente forte, mas funciona bem para o meu cenário.

Aqui está o meu projeto de teste: um aplicativo windows 8 app store com algumas chamadas de teste para a versão original de http://winrtstoragehelper.codeplex.com/ e minha versão modificada que usa o AsyncLock de Stephen Toub http: //blogs.msdn. com / b / pfxteam / archive / 2012/02/12 / 10266988.aspx .

Posso também sugerir este link: http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx

Isso é apenas uma extensão dessa resposta .

 using System; using System.Threading; using System.Threading.Tasks; public class SemaphoreLocker { private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); public async Task LockAsync(Func worker) { await _semaphore.WaitAsync(); try { await worker(); } finally { _semaphore.Release(); } } } 

Uso:

 public class Test { private static readonly SemaphoreLocker _locker = new SemaphoreLocker(); public async Task DoTest() { await _locker.LockAsync(async () => { // [asyn] calls can be used within this block // to handle a resource by one thread. }); } } 

Hmm, parece feio, parece funcionar.

 static class Async { public static Task Lock(object obj) { return TaskEx.Run(() => { var resetEvent = ResetEventFor(obj); resetEvent.WaitOne(); resetEvent.Reset(); return new ExitDisposable(obj) as IDisposable; }); } private static readonly IDictionary ResetEventMap = new Dictionary(); private static ManualResetEvent ResetEventFor(object @lock) { if (!ResetEventMap.ContainsKey(@lock) || !ResetEventMap[@lock].IsAlive) { ResetEventMap[@lock] = new WeakReference(new ManualResetEvent(true)); } return ResetEventMap[@lock].Target as ManualResetEvent; } private static void CleanUp() { ResetEventMap.Where(kv => !kv.Value.IsAlive) .ToList() .ForEach(kv => ResetEventMap.Remove(kv)); } private class ExitDisposable : IDisposable { private readonly object _lock; public ExitDisposable(object @lock) { _lock = @lock; } public void Dispose() { ResetEventFor(_lock).Set(); } ~ExitDisposable() { CleanUp(); } } } 

Stephen Taub implementou uma solução para essa questão, veja Construindo Primitivas de Coordenação Assíncronas, Parte 7: AsyncReaderWriterLock .

Stephen Taub é altamente considerado na indústria, então qualquer coisa que ele escreve é ​​provável que seja sólida.

Não vou reproduzir o código que ele postou em seu blog, mas vou mostrar como usá-lo:

 ///  /// Demo class for reader/writer lock that supports async/await. /// For source, see Stephen Taub's brilliant article, "Building Async Coordination /// Primitives, Part 7: AsyncReaderWriterLock". ///  public class AsyncReaderWriterLockDemo { private readonly IAsyncReaderWriterLock _lock = new AsyncReaderWriterLock(); public async void DemoCode() { using(var releaser = await _lock.ReaderLockAsync()) { // Insert reads here. // Multiple readers can access the lock simultaneously. } using (var releaser = await _lock.WriterLockAsync()) { // Insert writes here. // If a writer is in progress, then readers are blocked. } } } 

Se você usa um método integrado ao .NET framework, use SemaphoreSlim.WaitAsync . Você não terá um bloqueio de leitor / gravador, mas terá uma implementação testada e comprovada.

Eu tentei usar um monitor (código abaixo), que parece funcionar, mas tem um GOTCHA … quando você tem vários segmentos que dará … System.Threading.SynchronizationLockException O método de synchronization de object foi chamado de um bloco de código não sincronizado.

 using System; using System.Threading; using System.Threading.Tasks; namespace MyNamespace { public class ThreadsafeFooModifier : { private readonly object _lockObject; public async Task ModifyFooAsync() { FooResponse result; Monitor.Enter(_lockObject); try { result = await SomeFunctionToModifyFooAsync(); } finally { Monitor.Exit(_lockObject); } return result; } } } 

Antes disso, eu estava simplesmente fazendo isso, mas estava em um controlador ASP.NET, o que resultou em um deadlock.

public async Task ModifyFooAsync() { lock(lockObject) { return SomeFunctionToModifyFooAsync.Result; } }