¿Por qué no puedo usar el operador 'esperar' dentro del cuerpo de una declaración de bloqueo?

348

La palabra clave de espera en C # (.NET Async CTP) no está permitida desde una declaración de bloqueo.

De MSDN :

Una expresión en espera no se puede utilizar en una función síncrona, en una expresión de consulta, en el bloque catch o finalmente de una declaración de manejo de excepciones, en el bloque de una declaración de bloqueo o en un contexto inseguro.

Supongo que esto es difícil o imposible de implementar por el equipo del compilador por alguna razón.

Intenté solucionar el problema con la instrucción 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();
}

Sin embargo, esto no funciona como se esperaba. La llamada a Monitor.Exit dentro de ExitDisposable.Dispose parece bloquearse indefinidamente (la mayoría de las veces) causando puntos muertos a medida que otros hilos intentan adquirir el bloqueo. Sospecho que la falta de confiabilidad de mi trabajo y la razón por la cual las declaraciones no están permitidas en la declaración de bloqueo están de alguna manera relacionadas.

¿Alguien sabe por qué esperar no está permitido dentro del cuerpo de una declaración de bloqueo?

Kevin
fuente
27
Me imagino que encontraste la razón por la cual no está permitido.
asawyer
Estoy empezando a ponerme al día y aprender un poco más sobre la programación asíncrona. Después de numerosos puntos muertos en mis aplicaciones wpf, este artículo me pareció una gran protección en las prácticas de programación asíncrona. msdn.microsoft.com/en-us/magazine/…
C. Tewalt
El bloqueo está diseñado para evitar el acceso asíncrono cuando el acceso asincrónico rompería su código, por lo tanto, si está usando asíncrono dentro de un candado, ha invalidado su candado ... así que si necesita esperar algo dentro de su candado, no está usando el candado correctamente
MikeT

Respuestas:

366

Supongo que esto es difícil o imposible de implementar por el equipo del compilador por alguna razón.

No, no es nada difícil ni imposible de implementar; el hecho de que lo haya implementado usted mismo es un testimonio de ese hecho. Más bien, es una idea increíblemente mala y, por lo tanto, no lo permitimos, para protegerlo de cometer este error.

llame a Monitor.Exit dentro de ExitDisposable.Dispose parece bloquearse indefinidamente (la mayoría de las veces) causando puntos muertos a medida que otros hilos intentan adquirir el bloqueo. Sospecho que la falta de confiabilidad de mi trabajo y la razón por la cual las declaraciones no están permitidas en la declaración de bloqueo están de alguna manera relacionadas.

Correcto, has descubierto por qué lo hicimos ilegal. Aguardar dentro de una cerradura es una receta para producir puntos muertos.

Estoy seguro de que puede ver por qué: el código arbitrario se ejecuta entre el momento en que la espera devuelve el control a la persona que llama y se reanuda el método . Ese código arbitrario podría estar eliminando bloqueos que producen inversiones de orden de bloqueo y, por lo tanto, puntos muertos.

Peor aún, el código podría reanudarse en otro subproceso (en escenarios avanzados; normalmente se retoma el subproceso que esperaba, pero no necesariamente), en cuyo caso el desbloqueo estaría desbloqueando un bloqueo en un subproceso diferente al que tomó fuera de la cerradura. ¿Es eso una buena idea? No.

Observo que también es una "peor práctica" hacer un yield returninside a lock, por la misma razón. Es legal hacerlo, pero desearía que lo hubiéramos hecho ilegal. No vamos a cometer el mismo error para "esperar".

Eric Lippert
fuente
189
¿Cómo maneja un escenario en el que necesita devolver una entrada de caché, y si la entrada no existe, necesita calcular el contenido de forma asíncrona y luego agregar + devolver la entrada, asegurándose de que nadie más lo llame mientras tanto?
Softlion
99
Me doy cuenta de que llego tarde a la fiesta aquí, sin embargo, me sorprendió ver que pusiste puntos muertos como la razón principal por la que es una mala idea. Llegué a la conclusión en mi propio pensamiento de que la naturaleza reentrante del bloqueo / monitor sería una parte importante del problema. Es decir, debe poner en cola dos tareas en el grupo de subprocesos que lock (), que en un mundo sincrónico se ejecutarían en subprocesos separados. Pero ahora con espera (si me permite, quiero decir) podría tener dos tareas ejecutándose dentro del bloque de bloqueo porque el hilo se reutilizó. Produce hilaridad. ¿O entendí mal algo?
Gareth Wilson
44
@GarethWilson: hablé sobre puntos muertos porque la pregunta que se hizo fue sobre puntos muertos . Tiene razón en que los problemas extraños de reencuentro son posibles y parecen probables.
Eric Lippert
11
@Eric Lippert. Dado que la SemaphoreSlim.WaitAsyncclase se agregó al .NET Framework mucho después de que publicaste esta respuesta, creo que podemos asumir con seguridad que es posible ahora. Independientemente de esto, sus comentarios sobre la dificultad de implementar tal construcción siguen siendo completamente válidos.
Contango
77
"el código arbitrario se ejecuta entre el momento en que la espera devuelve el control a la persona que llama y se reanuda el método" - seguramente esto es cierto para cualquier código, incluso en ausencia de asíncrono / espera, en un contexto multiproceso: otros subprocesos pueden ejecutar código arbitrario en cualquier tiempo, y dicho código arbitrario como usted dice "podría estar eliminando bloqueos que producen inversiones de orden de bloqueo y, por lo tanto, puntos muertos". Entonces, ¿por qué es esto de particular importancia con async / wait? Entiendo que el segundo punto re "el código podría reanudarse en otro hilo" es de particular importancia para async / wait.
bacar
291

SemaphoreSlim.WaitAsyncMétodo de uso

 await mySemaphoreSlim.WaitAsync();
 try {
     await Stuff();
 } finally {
     mySemaphoreSlim.Release();
 }
usuario1639030
fuente
10
Como este método se introdujo recientemente en el marco .NET, creo que podemos suponer que el concepto de bloqueo en un mundo asíncrono / en espera ahora está bien probado.
Contango
55
Para obtener más información, busque el texto "SemaphoreSlim" en este artículo: Async / Await - Mejores prácticas en programación asincrónica
BobbyA
1
@JamesKo si todas esas tareas están esperando el resultado de StuffNo veo ninguna forma de evitarlo ...
Ohad Schneider
77
¿No debería inicializarse como mySemaphoreSlim = new SemaphoreSlim(1, 1)para que funcione lock(...)?
Sergey
3
Se agregó la versión extendida de esta respuesta: stackoverflow.com/a/50139704/1844247
Sergey
67

Básicamente, sería lo incorrecto hacer.

Hay dos maneras en que esto podría ser implementado:

  • Mantenga el bloqueo, soltándolo solo al final del bloque .
    Esta es una muy mala idea, ya que no sabes cuánto tiempo llevará la operación asincrónica. Solo debe mantener las cerraduras durante un tiempo mínimo . También es potencialmente imposible, ya que un subproceso posee un bloqueo, no un método, y es posible que ni siquiera ejecute el resto del método asincrónico en el mismo subproceso (dependiendo del programador de tareas).

  • Suelte el bloqueo en espera y vuelva a adquirirlo cuando regrese el espera
    Esto viola el principio de la menor OMI de asombro, donde el método asincrónico debe comportarse lo más parecido posible al código sincrónico equivalente, a menos que lo use Monitor.Waiten un bloque de bloqueo, espera poseer la cerradura por la duración del bloque.

Entonces, básicamente, hay dos requisitos en competencia aquí: no debe intentar hacer el primero aquí, y si desea tomar el segundo enfoque, puede hacer que el código sea mucho más claro al tener dos bloques de bloqueo separados separados por la expresión de espera:

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

Entonces, al prohibirle que espere en el bloque de bloqueo, el lenguaje lo obliga a pensar en lo que realmente quiere hacer y hace que esa elección sea más clara en el código que escribe.

Jon Skeet
fuente
55
Dado que la SemaphoreSlim.WaitAsyncclase se agregó al .NET Framework mucho después de que publicaste esta respuesta, creo que podemos asumir con seguridad que es posible ahora. Independientemente de esto, sus comentarios sobre la dificultad de implementar tal construcción siguen siendo completamente válidos.
Contango
77
@Contango: Bueno, eso no es exactamente lo mismo. En particular, el semáforo no está vinculado a un hilo específico. Logra objetivos similares al bloqueo, pero hay diferencias significativas.
Jon Skeet
@ JonSkeet Sé que este es un hilo muy antiguo y todo eso, pero no estoy seguro de cómo se protege la llamada a algo () usando esos bloqueos de la segunda manera. cuando un hilo está ejecutando algo () cualquier otro hilo puede involucrarse también en él. Me estoy perdiendo de algo ?
@Joseph: No está protegido en ese momento. Es el segundo enfoque, que deja en claro que está adquiriendo / liberando, luego adquiriendo / liberando nuevamente, posiblemente en un hilo diferente. Porque el primer enfoque es una mala idea, según la respuesta de Eric.
Jon Skeet
41

Esto es solo una extensión de esta respuesta .

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
fuente
1
Puede ser peligroso obtener el bloqueo del semáforo fuera del trybloque, si ocurre una excepción WaitAsyncy tryel semáforo nunca se liberará (punto muerto). Por otro lado, mover la WaitAsyncllamada al trybloque introducirá otro problema, cuando el semáforo puede liberarse sin que se adquiera un bloqueo. Vea el hilo relacionado donde se explicó este problema: stackoverflow.com/a/61806749/7889645
AndreyCh
16

Esto se refiere a http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx , http://winrtstoragehelper.codeplex.com/ , la tienda de aplicaciones de Windows 8 y .net 4.5

Aquí está mi ángulo sobre esto:

La función de lenguaje asíncrono / espera hace que muchas cosas sean bastante fáciles, pero también presenta un escenario que rara vez se encontraba antes de que fuera tan fácil de usar las llamadas asíncronas: reentrada.

Esto es especialmente cierto para los controladores de eventos, porque para muchos eventos no tienes idea de lo que está sucediendo después de regresar del controlador de eventos. Una cosa que podría suceder realmente es que el método asíncrono que está esperando en el primer controlador de eventos se llama desde otro controlador de eventos que todavía está en el mismo hilo.

Aquí hay un escenario real que encontré en una aplicación de la tienda de aplicaciones de Windows 8: Mi aplicación tiene dos marcos: entrando y saliendo de un marco que quiero cargar / guardar algunos datos en el archivo / almacenamiento. Los eventos OnNavigatedTo / From se utilizan para guardar y cargar. El guardado y la carga se realiza mediante alguna función de utilidad asíncrona (como http://winrtstoragehelper.codeplex.com/ ). Cuando se navega desde el cuadro 1 al cuadro 2 o en la otra dirección, se llama y espera la carga asíncrona y las operaciones seguras. Los controladores de eventos se vuelven asíncronos y devuelven void => no se pueden esperar.

Sin embargo, la primera operación de apertura de archivo (digamos: dentro de una función de guardar) de la utilidad también es asíncrona, por lo que la primera espera devuelve el control al marco, que en algún momento más tarde llama a la otra utilidad (carga) a través del segundo controlador de eventos. La carga ahora intenta abrir el mismo archivo y si el archivo ya está abierto para la operación de guardar, falla con una excepción ACCESSDENIED.

Una solución mínima para mí es asegurar el acceso a los archivos mediante un uso y un 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);
    }
}

Tenga en cuenta que su bloqueo básicamente bloquea todas las operaciones de archivo para la utilidad con solo un bloqueo, que es innecesariamente fuerte pero funciona bien para mi situación.

Aquí está mi proyecto de prueba: una aplicación de la tienda de aplicaciones de Windows 8 con algunas llamadas de prueba para la versión original de http://winrtstoragehelper.codeplex.com/ y mi versión modificada que usa AsyncLock de Stephen Toub http: //blogs.msdn. com / b / pfxteam / archive / 2012/02/12 / 10266988.aspx .

¿Puedo sugerir también este enlace: http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx

Hans
fuente
7

Stephen Taub ha implementado una solución a esta pregunta, consulte Creación de primitivas de coordinación asíncrona, Parte 7: AsyncReaderWriterLock .

Stephen Taub es muy apreciado en la industria, por lo que todo lo que escribe es probable que sea sólido.

No reproduciré el código que publicó en su blog, pero le mostraré cómo usarlo:

/// <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.
        }
    }
}

Si desea un método integrado en .NET Framework, use SemaphoreSlim.WaitAsyncen su lugar. No obtendrá un bloqueo de lector / escritor, pero obtendrá una implementación probada.

Aplazamiento de pago
fuente
Tengo curiosidad por saber si hay alguna advertencia al usar este código. Si alguien puede demostrar algún problema con este código, me gustaría saberlo. Sin embargo, lo que es cierto es que el concepto de bloqueo asíncrono / espera está definitivamente bien probado, como lo SemaphoreSlim.WaitAsyncestá en el marco .NET. Todo lo que hace este código es agregar un concepto de bloqueo de lector / escritor.
Contango
3

Hmm, se ve feo, 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
fuente
0

Intenté usar un Monitor (código a continuación) que parece funcionar pero tiene un GOTCHA ... cuando tiene varios subprocesos, dará ... System.Threading.SynchronizationLockException El método de sincronización de objetos se llamó desde un bloque de código no 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 de esto, simplemente estaba haciendo esto, pero estaba en un controlador ASP.NET, por lo que resultó en un punto muerto.

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

Andrew Paté
fuente