c# create - ¿Por qué no puedo usar el operador 'await' dentro del cuerpo de una declaración de bloqueo?




async method (8)

La palabra clave await en C # (.NET Async CTP) no está permitida dentro de una sentencia de bloqueo.

Desde MSDN :

Una expresión de espera no puede usarse en una función síncrona, en una expresión de consulta, en el retén o en el último bloque de una declaración de manejo de excepciones, en el bloque de una sentencia de bloqueo o en un contexto inseguro.

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

Intenté un trabajo alrededor con la declaración de uso:

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 bloquear indefinidamente (la mayoría de las veces) causando interbloqueos cuando otros subprocesos intentan adquirir el bloqueo. Sospecho que la falta de fiabilidad de mi trabajo y la razón por la que las declaraciones de espera no están permitidas en la declaración de bloqueo están relacionadas de alguna manera.

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


Answers

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

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

No reproduciré el código que él publicó en su blog, pero 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 tiene un método que está integrado en el marco .NET, use SemaphoreSlim.WaitAsync lugar. No obtendrá un bloqueo de lector / escritor, pero obtendrá una implementación probada y comprobada.


Intenté usar un Monitor (código a continuación) que parece funcionar pero tiene un GOTCHA ... cuando tiene varios subprocesos le dará ... System.Threading.SynchronizationLockException Se llamó al método de sincronización de objetos 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; } }


Hmm, parece 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();
        }
    }
}

Esto se refiere a 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 en esto:

La función de lenguaje async / await 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 usar llamadas asíncronas: la reentrada.

Esto es especialmente cierto para los controladores de eventos, ya que para muchos eventos no tiene idea de lo que está sucediendo después de regresar del controlador de eventos. Una cosa que realmente podría suceder es que el método asíncrono que está esperando en el primer controlador de eventos, recibe una llamada desde otro controlador de eventos que aún se encuentra en el mismo hilo.

Este es un escenario real que encontré en una aplicación de Windows 8 App Store: mi aplicación tiene dos marcos: entrando y saliendo de un marco que quiero cargar / guardar algunos datos en un archivo / almacenamiento. Los eventos OnNavigatedTo / From se utilizan para guardar y cargar. El guardado y la carga se realizan mediante alguna función de utilidad asíncrona (como http://winrtstoragehelper.codeplex.com/ ). Al navegar desde el cuadro 1 al cuadro 2 o en la otra dirección, se llama y se espera la carga asíncrona y las operaciones seguras. Los manejadores de eventos se vuelven asíncronos devolviendo el vacío => no se pueden esperar.

Sin embargo, la primera operación de apertura de archivo (digamos: dentro de una función de guardado) de la utilidad también es asíncrona, por lo que la primera espera devuelve el control al marco, que 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 un solo bloqueo, lo cual es innecesariamente sólido pero funciona bien para mi escenario.

Here 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 el AsyncLock de Stephen Toub blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx .

También puedo sugerir este enlace: hanselman.com/blog/…


Utilice el método SemaphoreSlim.WaitAsync .

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

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

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

La llamada a Monitor.Exit dentro de ExitDisposable.Dispose parece bloquear indefinidamente (la mayoría de las veces) causando interbloqueos mientras otros hilos intentan adquirir el bloqueo. Sospecho que la falta de fiabilidad de mi trabajo y la razón por la que las declaraciones de espera no están permitidas en la declaración de bloqueo están relacionadas de alguna manera.

Correcto, has descubierto por qué lo hicimos ilegal. Esperar 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 el método se reanuda . Ese código arbitrario podría estar eliminando bloqueos que producen inversiones de ordenamiento de bloqueo, y por lo tanto puntos muertos.

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

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


Básicamente sería lo incorrecto hacer.

Hay dos formas en que esto podría implementarse:

  • Mantenga la cerradura, soltándola solo al final del bloque .
    Esta es una idea realmente mala, ya que no sabe cuánto tiempo tomará la operación asíncrona. Solo debes mantener los candados por un tiempo mínimo . También es potencialmente imposible, ya que un subproceso posee un bloqueo, no un método, y ni siquiera puede ejecutar el resto del método asíncrono en el mismo subproceso (dependiendo del programador de tareas).

  • Libere el bloqueo en espera y vuelva a adquirirlo cuando vuelva la espera.
    Esto viola el principio de IMO de menor asombro, donde el método asíncrono debe comportarse lo más cerca posible del código síncrono equivalente, a menos que use Monitor.Wait en un bloqueo, espera poseer el bloqueo durante la duración del bloqueo.

Básicamente, hay dos requisitos que compiten entre sí: no deberías intentar hacer el primero aquí, y si deseas adoptar el segundo enfoque, puedes 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)
{
}

Por lo tanto, al prohibirle que espere en el bloqueo de bloqueo en sí, el lenguaje lo obliga a pensar qué es lo que realmente quiere hacer y hacer que esa elección sea más clara en el código que escribe.


"Esto no compilará y creo que tiene mucho sentido"

Bueno, creo que no.

Cuando literalmente tienes catch(Exception) entonces no necesitas el finally (y probablemente ni siquiera el continue ).

Cuando tiene la catch(SomeException) más realista catch(SomeException) , ¿qué debería pasar cuando no se catch(SomeException) una excepción? Tu continue quiere ir de una manera, la excepción manejando otra.





c# .net async-await