¿Por qué no puedo utilizar el operador 'await' dentro del cuerpo de una declaración de bloqueo?
La await
palabra clave en C# (.NET Async CTP) no está permitida dentro de una lock
declaración.
De MSDN :
Una expresión de 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 para el equipo compilador por alguna razón.
Intenté solucionar el problema 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 Monitor.Exit
interna ExitDisposable.Dispose
parece bloquearse indefinidamente (la mayor parte del tiempo), provocando interbloqueos cuando otros subprocesos intentan adquirir el bloqueo. Sospecho que la falta de confiabilidad de mi trabajo y la razón por la que await
no se permiten lock
declaraciones están relacionadas de alguna manera.
¿Alguien sabe por qué await
no se permite dentro del cuerpo de una lock
declaración?
Supongo que esto es difícil o imposible de implementar para el equipo compilador por alguna razón.
No, no es en absoluto difícil o imposible de implementar; el hecho de que usted mismo lo haya implementado es un testimonio de ese hecho. Más bien, es una idea increíblemente mala y por eso no la permitimos, para protegerlo de cometer este error.
La llamada a Monitor.Exit dentro de ExitDisposable.Dispose parece bloquearse indefinidamente (la mayor parte del tiempo), provocando interbloqueos cuando otros subprocesos intentan adquirir el bloqueo. Sospecho que la falta de confiabilidad de mi solución y el motivo por el cual las declaraciones de espera no están permitidas en la declaración de bloqueo están relacionados 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 puedes ver por qué: se ejecuta código arbitrario entre el momento en que await 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 en el orden de los bloqueos y, por lo tanto, interbloqueos.
Peor aún, el código podría reanudarse en otro hilo (en escenarios avanzados; normalmente retoma el hilo que hizo la espera, pero no necesariamente), en cuyo caso el desbloqueo sería desbloquear un bloqueo en un hilo diferente al hilo que tomó sacar la cerradura. ¿Es eso una buena idea? No.
Observo que también es una "peor práctica" hacer un yield return
inside a lock
, por la misma razón. Es legal hacerlo, pero desearía que lo hubiésemos hecho ilegal. No vamos a cometer el mismo error con "esperar".
Utilice el SemaphoreSlim.WaitAsync
método.
await mySemaphoreSlim.WaitAsync();
try {
await Stuff();
} finally {
mySemaphoreSlim.Release();
}
Esta es solo una extensión de esta respuesta del usuario1639030.
Versión básica
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();
}
}
// overloading variant for non-void methods with return type (generic T)
public async Task<T> LockAsync<T>(Func<Task<T>> worker)
{
await _semaphore.WaitAsync();
try
{
return await worker();
}
finally
{
_semaphore.Release();
}
}
}
Uso:
public class Test
{
private static readonly SemaphoreLocker _locker = new SemaphoreLocker();
public async Task DoTest()
{
await _locker.LockAsync(async () =>
{
// [async] calls can be used within this block
// to handle a resource by one thread.
});
// OR
var result = await _locker.LockAsync(async () =>
{
// [async] calls can be used within this block
// to handle a resource by one thread.
});
}
}
Versión extendida
Una versión del LockAsync
método que afirma ser completamente seguro para interbloqueos (de la cuarta revisión sugerida por Jez).
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)
{
var isTaken = false;
try
{
do
{
try
{
}
finally
{
isTaken = await _semaphore.WaitAsync(TimeSpan.FromSeconds(1));
}
}
while (!isTaken);
await worker();
}
finally
{
if (isTaken)
{
_semaphore.Release();
}
}
}
// overloading variant for non-void methods with return type (generic T)
public async Task<T> LockAsync<T>(Func<Task<T>> worker)
{
var isTaken = false;
try
{
do
{
try
{
}
finally
{
isTaken = await _semaphore.WaitAsync(TimeSpan.FromSeconds(1));
}
}
while (!isTaken);
return await worker();
}
finally
{
if (isTaken)
{
_semaphore.Release();
}
}
}
}
Uso:
public class Test
{
private static readonly SemaphoreLocker _locker = new SemaphoreLocker();
public async Task DoTest()
{
await _locker.LockAsync(async () =>
{
// [async] calls can be used within this block
// to handle a resource by one thread.
});
// OR
var result = await _locker.LockAsync(async () =>
{
// [async] calls can be used within this block
// to handle a resource by one thread.
});
}
}