Por que não consigo usar o operador 'aguardar' no corpo de uma instrução de bloqueio?

348

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

Do MSDN :

Uma expressão de espera não pode ser usada em uma função síncrona, em uma expressão de consulta, no bloco catch ou finalmente de uma instrução de tratamento de exceção, no bloco de uma instrução de bloqueio ou em um contexto não seguro.

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

Tentei uma solução alternativa com a instrução using:

class Async
{
    public static async Task<IDisposable> 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 conforme o esperado. A chamada para Monitor.Exit em ExitDisposable.Dispose parece bloquear indefinidamente (na maioria das vezes) causando bloqueios, enquanto outros threads tentam adquirir o bloqueio. Suspeito que a falta de confiabilidade do meu trabalho e a razão pela qual as instruções de espera não são permitidas na instrução de bloqueio estejam de alguma forma relacionadas.

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

Kevin
fonte
27
Eu imagino que você encontrou a razão pela qual não é permitido.
asawyer
Estou começando a acompanhar e aprender um pouco mais sobre programação assíncrona. Após inúmeros bloqueios em meus aplicativos wpf, achei este artigo uma ótima proteção para práticas de programação assíncrona. msdn.microsoft.com/en-us/magazine/…
C. Tewalt
O bloqueio foi desenvolvido para impedir o acesso assíncrono quando o acesso assíncrono violar o seu código; portanto, se você estiver usando o assíncrono dentro de um bloqueio, você invalidará o seu bloqueio. Portanto, se você precisar aguardar algo dentro do seu bloqueio, não o
usará

Respostas:

366

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

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

chamada para Monitor.Exit em ExitDisposable.Dispose parece bloquear indefinidamente (na maioria das vezes) causando deadlocks, enquanto outros threads tentam adquirir o bloqueio. Suspeito que a falta de confiabilidade do meu trabalho e a razão pela qual as instruções de espera não são permitidas na instrução de bloqueio estejam de alguma forma relacionadas.

Correto, você descobriu por que tornamos ilegal. Aguardar dentro de uma fechadura é uma receita para produzir impasses.

Tenho certeza de que você pode ver o motivo: o código arbitrário é executado entre o tempo em que o aguardado retorna o controle ao chamador e o método é retomado . Esse código arbitrário pode estar desativando bloqueios que produzem inversões de ordem de bloqueio e, portanto, bloqueios.

Pior, o código pode continuar em outro encadeamento (em cenários avançados; normalmente você retoma o encadeamento que aguardava, mas não necessariamente); nesse caso, o desbloqueio desbloqueia um bloqueio em um encadeamento diferente do encadeamento que levou fora da fechadura. Essa é uma boa ideia? Não.

Observo que também é uma "pior prática" fazer um yield returndentro de um lock, pela mesma razão. É legal fazer isso, mas eu gostaria que tivéssemos tornado ilegal. Não vamos cometer o mesmo erro de "aguardar".

Eric Lippert
fonte
189
Como você lida com um cenário em que precisa retornar uma entrada de cache e, se a entrada não existir, você precisa calcular de forma assíncrona o conteúdo e adicionar + retornar a entrada, certificando-se de que ninguém mais ligue enquanto isso?
Softlion
9
Sei que estou atrasado para a festa aqui, no entanto, fiquei surpreso ao ver que você coloca os impasses como a principal razão pela qual essa é uma má ideia. Eu tinha chegado à conclusão em meu próprio pensamento de que a natureza reentrante de lock / Monitor seria uma parte maior do problema. Ou seja, você enfileira duas tarefas no conjunto de encadeamentos que lock (), que em um mundo síncrono seria executado em encadeamentos separados. Mas agora com aguardar (se permitido, quero dizer), você pode ter duas tarefas em execução no bloco de bloqueio porque o encadeamento foi reutilizado. Hilaridade segue. Ou entendi algo errado?
21812 Gareth Wilson #:
4
@GarethWilson: Eu falei sobre impasses, porque a pergunta era sobre impasses . Você está certo de que problemas bizarros de reentrada são possíveis e parecem prováveis.
Eric Lippert
11
@Eric Lippert. Dado que a SemaphoreSlim.WaitAsyncclasse foi adicionada à estrutura .NET bem depois que você postou esta resposta, acho que podemos assumir com segurança que isso é possível agora. Independentemente disso, seus comentários sobre a dificuldade de implementar essa construção ainda são totalmente válidos.
Contango 12/09/14
7
"código arbitrário é executado entre o momento em que a espera retorna o controle para o chamador e o método é retomado" - certamente isso é verdade para qualquer código, mesmo na ausência de assíncrono / espera, em um contexto multithread: outros threads podem executar código arbitrário a qualquer momento time e o código arbitrário, como você diz "podem estar bloqueando bloqueios que produzem inversões na ordem de bloqueio e, portanto, bloqueios". Então, por que isso é de particular importância com async / wait? Entendo que o segundo ponto é "o código pode ser retomado em outro segmento", sendo particularmente importante para assíncrono / aguardar.
Bacar
291

Use o SemaphoreSlim.WaitAsyncmétodo

 await mySemaphoreSlim.WaitAsync();
 try {
     await Stuff();
 } finally {
     mySemaphoreSlim.Release();
 }
user1639030
fonte
10
Como esse método foi introduzido recentemente no framework .NET, acho que podemos assumir que o conceito de bloqueio em um mundo assíncrono / aguardado agora está bem comprovado.
Contango 12/09/14
5
Para mais algumas informações, procure o texto "SemaphoreSlim" neste artigo: Async / Await - Melhores Práticas em Programação Asynchronous
BobbyA
11
@JamesKo se todas essas tarefas estão esperando o resultado de Stuffeu não vejo qualquer maneira de contornar isso ...
Ohad Schneider
7
Não deveria ser inicializado como mySemaphoreSlim = new SemaphoreSlim(1, 1)para funcionar como lock(...)?
Sergey
3
Adicionada a versão estendida desta resposta: stackoverflow.com/a/50139704/1844247
Sergey
67

Basicamente, seria a coisa errada a fazer.

Há duas maneiras este poderia ser implementadas:

  • Segure a trava, liberando-a apenas no final do bloco .
    Essa é uma péssima idéia, pois você não sabe quanto tempo a operação assíncrona levará. Você só deve reter bloqueios por um período mínimo de tempo. Também é potencialmente impossível, pois um encadeamento possui um bloqueio, não um método - e você pode nem executar o restante do método assíncrono no mesmo encadeamento (dependendo do agendador de tarefas).

  • Solte o bloqueio no aguarde e recupere-o quando o aguardar retornar
    Isso viola o princípio da IMO de menor espanto, em que o método assíncrono deve se comportar o mais próximo possível do código síncrono equivalente - a menos que você use Monitor.Waitem um bloqueio, espere possui a trava pela duração do bloco.

Portanto, basicamente existem dois requisitos concorrentes aqui - você não deve tentar fazer o primeiro aqui e, se quiser adotar a segunda abordagem, poderá tornar o código muito mais claro, com dois blocos de bloqueio separados pela expressão de espera:

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

Portanto, ao proibi-lo de aguardar no próprio bloco de bloqueio, o idioma está forçando você a pensar no que realmente deseja fazer e tornando essa opção mais clara no código que você escreve.

Jon Skeet
fonte
5
Dado que a SemaphoreSlim.WaitAsyncclasse foi adicionada à estrutura .NET bem depois que você postou esta resposta, acho que podemos assumir com segurança que isso é possível agora. Independentemente disso, seus comentários sobre a dificuldade de implementar essa construção ainda são totalmente válidos.
Contango 12/09/14
7
@ Contango: Bem, isso não é exatamente a mesma coisa. Em particular, o semáforo não está vinculado a um segmento específico. Atinge objetivos semelhantes ao bloqueio, mas há diferenças significativas.
22814 Jon Skeet
@ JonSkeet eu sei que este é um thread muito antigo e tudo, mas não tenho certeza de como a chamada something () é protegida usando esses bloqueios da segunda maneira? Quando um thread está executando algo (), qualquer outro thread também pode se envolver! Estou faltando alguma coisa aqui?
@ Joseph: Não está protegido nesse ponto. É a segunda abordagem, que deixa claro que você está adquirindo / liberando e adquirindo / liberando novamente, possivelmente em um encadeamento diferente. Porque a primeira abordagem é uma má ideia, conforme a resposta de Eric.
Jon Skeet
41

Esta é apenas uma extensão para esta 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<Task> 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. 
        });
    }
}
Sergey
fonte
11
Pode ser perigoso para obter o bloqueio de semáforo fora do trybloco - se uma exceção acontece entre WaitAsynce tryo semáforo nunca será liberado (impasse). Por outro lado, mover a WaitAsyncchamada para o trybloco apresentará outro problema, quando o semáforo pode ser liberado sem que um bloqueio seja adquirido. Consulte o segmento relacionado onde esse problema foi explicado: stackoverflow.com/a/61806749/7889645
AndreyCh
16

Refere-se 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 assíncrona / aguardada facilita muitas coisas, mas também apresenta um cenário que raramente era encontrado antes de ser tão fácil usar chamadas assíncronas: reentrada.

Isso é especialmente verdadeiro para os manipuladores de eventos, porque para muitos eventos você não tem idéia do que está acontecendo depois que você retorna do manipulador de eventos. Uma coisa que pode realmente acontecer é que o método assíncrono que você está aguardando no primeiro manipulador de eventos é chamado de outro manipulador de eventos ainda no mesmo encadeamento.

Aqui está um cenário real que encontrei em um aplicativo da App Store do Windows 8: Meu aplicativo tem dois quadros: entrando e saindo de um quadro. Quero carregar / proteger alguns dados em um arquivo / armazenamento. Os eventos OnNavigatedTo / From são usados ​​para salvar e carregar. O salvamento e o carregamento são feitos por alguma função do utilitário assíncrono (como http://winrtstoragehelper.codeplex.com/ ). Ao navegar do quadro 1 para o quadro 2 ou na outra direção, a carga assíncrona e as operações seguras são chamadas e aguardadas. Os manipuladores de eventos tornam-se assíncronos retornando nulos => eles não podem ser aguardados.

No entanto, a primeira operação de abertura de arquivo (digamos: dentro de uma função de salvamento) do utilitário também é assíncrona e, portanto, a primeira espera retorna o controle para a estrutura, que mais tarde chama o outro utilitário (carga) por meio do segundo manipulador de eventos. O carregamento agora tenta abrir o mesmo arquivo e, se o arquivo já estiver aberto para a operação de salvamento, falhará com uma exceção ACCESSDENIED.

Uma solução mínima para mim é proteger o acesso ao arquivo usando um AsyncLock e um.

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);
    }
}

Observe que o bloqueio dele basicamente bloqueia todas as operações de arquivo do utilitário com apenas um bloqueio, o que é desnecessariamente forte, mas funciona bem para o meu cenário.

Aqui está o meu projeto de teste: um aplicativo da 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 .

Também posso sugerir este link: http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx

Hans
fonte
7

Stephen Taub implementou uma solução para esta pergunta, consulte Criando primitivas de coordenação assíncrona, parte 7: AsyncReaderWriterLock .

Stephen Taub é altamente considerado no setor, então qualquer coisa que ele escreva provavelmente será sólido.

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

/// <summary>
///     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".
/// </summary>
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ê deseja um método inserido na estrutura .NET, use-o SemaphoreSlim.WaitAsync. Você não obterá um bloqueio de leitor / gravador, mas obterá uma implementação testada e testada.

Contango
fonte
Estou curioso para saber se existem algumas ressalvas no uso desse código. Se alguém puder demonstrar algum problema com esse código, eu gostaria de saber. No entanto, o que é verdade é que o conceito de bloqueio assíncrono / aguardado é definitivamente bem comprovado, como SemaphoreSlim.WaitAsyncna estrutura .NET. Tudo que esse código faz é adicionar um conceito de bloqueio de leitor / gravador.
Contango 12/09
3

Hmm, parece feio, parece funcionar.

static class Async
{
    public static Task<IDisposable> Lock(object obj)
    {
        return TaskEx.Run(() =>
            {
                var resetEvent = ResetEventFor(obj);

                resetEvent.WaitOne();
                resetEvent.Reset();

                return new ExitDisposable(obj) as IDisposable;
            });
    }

    private static readonly IDictionary<object, WeakReference> ResetEventMap =
        new Dictionary<object, WeakReference>();

    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();
        }
    }
}
Anton Pogonets
fonte
0

Eu tentei usar um Monitor (código abaixo) que parece funcionar, mas tem um GOTCHA ... quando você tem vários threads, ele fornecerá ... System.Threading.SynchronizationLockException O método de sincronização de objetos foi chamado a partir 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<FooResponse> 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, resultando em um impasse.

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

andrew pate
fonte