Perché non riesco a utilizzare l'operatore 'wait' all'interno del corpo di un'istruzione lock?


348

La parola chiave wait in C # (.NET Async CTP) non è consentita all'interno di un'istruzione lock.

Da MSDN :

Un'espressione waitit non può essere utilizzata in una funzione sincrona, in un'espressione di query, nel blocco catch o infine di un'istruzione di gestione delle eccezioni, nel blocco di un'istruzione lock o in un contesto non sicuro.

Presumo che questo sia difficile o impossibile per il team del compilatore per qualche motivo.

Ho tentato di aggirare l'istruzione 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();
}

Tuttavia, questo non funziona come previsto. La chiamata a Monitor.Exit in ExitDisposable.Dispose sembra bloccarsi indefinitamente (la maggior parte delle volte) causando deadlock mentre altri thread tentano di acquisire il blocco. Sospetto che l'inaffidabilità del mio lavoro in giro e il motivo per cui le dichiarazioni di attesa non sono consentite nell'istruzione lock sono in qualche modo correlate.

Qualcuno sa perché waitit non è consentito nel corpo di un'istruzione lock?


27
Immagino tu abbia trovato il motivo per cui non è permesso.
asawyer,


Sto solo iniziando a recuperare e imparare un po 'di più sulla programmazione asincrona. Dopo numerosi deadlock nelle mie applicazioni wpf, ho trovato questo articolo una grande protezione nelle pratiche di programmazione asincrona. msdn.microsoft.com/en-us/magazine/…
C. Tewalt

Lock è progettato per impedire l'accesso asincrono quando l'accesso asincrono violerebbe il tuo codice, ergo se stai usando asincrono all'interno di un blocco hai invalidato il tuo blocco .. quindi se devi aspettare qualcosa all'interno del tuo blocco non stai usando il blocco correttamente
MikeT

Risposte:


366

Presumo che questo sia difficile o impossibile per il team del compilatore per qualche motivo.

No, non è affatto difficile o impossibile da attuare - il fatto che tu l'abbia implementato da solo è una testimonianza di questo fatto. Piuttosto, è un'idea incredibilmente cattiva e quindi non lo permettiamo, in modo da proteggerti dal commettere questo errore.

chiama a Monitor.Exit in ExitDisposable.Dispose sembra bloccarsi indefinitamente (il più delle volte) causando deadlock mentre altri thread tentano di acquisire il lock. Sospetto che l'inaffidabilità del mio lavoro in giro e il motivo per cui le dichiarazioni di attesa non sono consentite nell'istruzione lock sono in qualche modo correlate.

Esatto, hai scoperto perché l'abbiamo reso illegale. Attendere all'interno di una serratura è una ricetta per produrre deadlock.

Sono sicuro che puoi capire perché: il codice arbitrario viene eseguito tra il momento in cui l'attesa restituisce il controllo al chiamante e il metodo riprende . Tale codice arbitrario potrebbe eliminare i blocchi che producono inversioni di ordinamento dei blocchi e quindi deadlock.

Peggio ancora, il codice potrebbe riprendere su un altro thread (in scenari avanzati; normalmente si riprende sul thread che ha atteso, ma non necessariamente) nel qual caso lo sblocco sarebbe sbloccare un blocco su un thread diverso rispetto al thread che ha preso fuori dalla serratura. È una buona idea? No.

Noto che è anche una "peggior pratica" fare un yield returninterno a lock, per lo stesso motivo. È legale farlo, ma vorrei che lo avessimo reso illegale. Non faremo lo stesso errore di "wait".


189
Come si gestisce uno scenario in cui è necessario restituire una voce della cache e se la voce non esiste è necessario calcolare il contenuto in modo asincrono, quindi aggiungere + restituire la voce, assicurandosi che nessun altro ti chiami nel frattempo?
Softlion,

9
Mi rendo conto di essere in ritardo alla festa qui, tuttavia sono stato sorpreso di vedere che hai messo i deadlock come la ragione principale per cui questa è una cattiva idea. Ero giunto alla conclusione nel mio pensiero che la natura rientrante di lock / Monitor sarebbe stata una parte maggiore del problema. Cioè, si mettono in coda due attività al pool di thread che lock (), che in un mondo sincrono verrebbe eseguito su thread separati. Ma ora con wait (se consentito, intendo) potresti avere due compiti in esecuzione all'interno del blocco di blocco perché il thread è stato riutilizzato. Ne deriva l'ilarità. O ho frainteso qualcosa?
Gareth Wilson,

4
@GarethWilson: ho parlato di deadlock perché la domanda posta riguardava deadlock . Hai ragione sul fatto che bizzarri problemi di rientro sono possibili e sembrano probabili.
Eric Lippert,

11
@Eric Lippert. Dato che la SemaphoreSlim.WaitAsyncclasse è stata aggiunta al framework .NET ben dopo aver pubblicato questa risposta, penso che possiamo tranquillamente supporre che sia possibile ora. Indipendentemente da ciò, i tuoi commenti sulla difficoltà di implementare un tale costrutto sono ancora del tutto validi.
Contango,

7
"il codice arbitrario viene eseguito tra il momento in cui l'attesa restituisce il controllo al chiamante e il metodo riprende" - sicuramente questo vale per qualsiasi codice, anche in assenza di asincronizzazione / attesa, in un contesto multithread: altri thread possono eseguire codice arbitrario in qualsiasi tempo, e ha detto un codice arbitrario come dici "potrebbe essere rimuovere i blocchi che producono inversioni di ordinamento dei blocchi, e quindi deadlock". Quindi perché questo ha un significato particolare con asincrono / wait? Capisco il secondo punto in cui "il codice potrebbe riprendere su un altro thread" di essere di particolare significato per asincronizzare / attendere.
bacar,

291

Usa il SemaphoreSlim.WaitAsyncmetodo

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

10
Dato che questo metodo è stato recentemente introdotto nel framework .NET, penso che possiamo supporre che il concetto di blocco in un mondo asincrono / attende sia ora ben collaudato.
Contango,

5
Per ulteriori informazioni, cerca il testo "SemaphoreSlim" in questo articolo: Async / Await - Best Practices in Asynchronous Programming
BobbyA

1
@JamesKo se tutti quei compiti sono in attesa del risultato di Stuffnon vedo alcun modo per aggirarlo ...
Ohad Schneider,

7
Non dovrebbe essere inizializzato come mySemaphoreSlim = new SemaphoreSlim(1, 1)per funzionare come lock(...)?
Sergey,

3
Aggiunta la versione estesa di questa risposta: stackoverflow.com/a/50139704/1844247
Sergey

67

Fondamentalmente sarebbe la cosa sbagliata da fare.

Ci sono due modi in cui questo potrebbe essere implementato:

  • Tenere premuto il blocco, rilasciandolo solo alla fine del blocco .
    Questa è una pessima idea poiché non sai quanto tempo impiegherà l'operazione asincrona. Dovresti tenere i blocchi solo per un periodo di tempo minimo . È anche potenzialmente impossibile, poiché un thread possiede un lock, non un metodo - e potresti anche non eseguire il resto del metodo asincrono sullo stesso thread (a seconda dell'utilità di pianificazione).

  • Rilascia il lucchetto nell'attesa e riacquistalo quando l'attesa ritorna.
    Ciò viola il principio di IMO di minimo stupore, in cui il metodo asincrono dovrebbe comportarsi il più vicino possibile come il codice sincrono equivalente - a meno che non lo utilizzi Monitor.Waitin un blocco di blocco, ti aspetti di possedere il blocco per la durata del blocco.

Quindi sostanzialmente ci sono due requisiti in competizione qui: non dovresti provare a fare il primo qui, e se vuoi prendere il secondo approccio puoi rendere il codice molto più chiaro avendo due blocchi di blocco separati separati dall'espressione wait:

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

Quindi, vietandoti di aspettare nel blocco stesso, la lingua ti costringe a pensare a ciò che vuoi davvero fare e rende più chiara quella scelta nel codice che scrivi.


5
Dato che la SemaphoreSlim.WaitAsyncclasse è stata aggiunta al framework .NET ben dopo aver pubblicato questa risposta, penso che possiamo tranquillamente supporre che sia possibile ora. Indipendentemente da ciò, i tuoi commenti sulla difficoltà di implementare un tale costrutto sono ancora del tutto validi.
Contango,

7
@Contango: Beh, non è esattamente la stessa cosa. In particolare, il semaforo non è legato a un thread specifico. Raggiunge obiettivi simili da bloccare, ma ci sono differenze significative.
Jon Skeet,

@JonSkeet So che questo è un thread molto vecchio e tutto il resto, ma non sono sicuro di come la chiamata a qualcosa () sia protetta usando quei blocchi nel secondo modo? quando un thread sta eseguendo qualcosa () anche qualsiasi altro thread può essere coinvolto in esso! Mi sto perdendo qualcosa qui?

@Joseph: non è protetto a quel punto. È il secondo approccio, che chiarisce che stai acquisendo / rilasciando, quindi acquisendo / rilasciando di nuovo, possibilmente su un thread diverso. Perché il primo approccio è una cattiva idea, secondo la risposta di Eric.
Jon Skeet,

41

Questa è solo un'estensione di questa risposta .

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

1
Può essere pericoloso ottenere il blocco del semaforo all'esterno del tryblocco - se si verifica un'eccezione tra il semaforo WaitAsynce tryil semaforo non verrà mai rilasciato (deadlock). D'altra parte, spostare la WaitAsyncchiamata nel tryblocco introduce un altro problema, quando il semaforo può essere rilasciato senza che sia stato acquisito un blocco. Vedere il thread correlato in cui è stato spiegato questo problema: stackoverflow.com/a/61806749/7889645
AndreyCh

16

Questo si riferisce a http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx , http://winrtstoragehelper.codeplex.com/ , app store di Windows 8 e .net 4.5

Ecco il mio punto di vista su questo:

La funzione di linguaggio asincrono / wait rende molte cose abbastanza facili ma introduce anche uno scenario che raramente si incontrava prima che fosse così facile usare le chiamate asincrone: rientro.

Ciò è particolarmente vero per i gestori di eventi, perché per molti eventi non hai idea di cosa accada dopo il tuo ritorno dal gestore di eventi. Una cosa che potrebbe effettivamente accadere è che il metodo asincrono che stai aspettando nel primo gestore eventi, venga chiamato da un altro gestore eventi ancora sullo stesso thread.

Ecco uno scenario reale che mi sono imbattuto in un'app store di Windows 8: La mia app ha due frame: entrare e uscire da un frame che voglio caricare / salvare alcuni dati su file / archiviazione. Gli eventi OnNavigatedTo / From vengono utilizzati per il salvataggio e il caricamento. Il salvataggio e il caricamento vengono eseguiti da alcune funzioni dell'utilità asincrona (come http://winrtstoragehelper.codeplex.com/ ). Durante la navigazione dal fotogramma 1 al fotogramma 2 o nella direzione opposta, il carico asincrono e le operazioni sicure vengono chiamati e attesi. I gestori di eventi diventano asincroni restituendo void => non possono essere attesi.

Tuttavia, anche la prima operazione di apertura del file (diciamo: all'interno di una funzione di salvataggio) dell'utilità è asincrona e quindi la prima attesa restituisce il controllo al framework, che in seguito chiama l'altra utilità (carica) tramite il secondo gestore di eventi. Il caricamento ora tenta di aprire lo stesso file e se il file è già aperto per l'operazione di salvataggio, non riesce con un'eccezione ACCESSDENIED.

Una soluzione minima per me è quella di proteggere l'accesso ai file tramite un using e 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);
    }
}

Si noti che il suo blocco sostanzialmente blocca tutte le operazioni sui file per l'utilità con un solo blocco, che è inutilmente forte ma funziona bene per il mio scenario.

Ecco il mio progetto di test: un'app store di Windows 8 con alcune chiamate di prova per la versione originale da http://winrtstoragehelper.codeplex.com/ e la mia versione modificata che utilizza AsyncLock di Stephen Toub http: //blogs.msdn. com / b / pfxteam / archive / 2012/02/12 / 10266988.aspx .

Posso anche suggerire questo link: http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx


7

Stephen Taub ha implementato una soluzione a questa domanda, vedi Creazione di primitive di coordinamento asincrono, parte 7: AsyncReaderWriterLock .

Stephen Taub è molto apprezzato nel settore, quindi è probabile che tutto ciò che scrive sia solido.

Non riprodurrò il codice che ha pubblicato sul suo blog, ma ti mostrerò come 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.
        }
    }
}

Se si desidera un metodo inserito nel framework .NET, utilizzare SemaphoreSlim.WaitAsyncinvece. Non otterrai un blocco lettore / scrittore, ma avrai l'implementazione provata e testata.


Sono curioso di sapere se ci sono avvertenze sull'utilizzo di questo codice. Se qualcuno può dimostrare problemi con questo codice, mi piacerebbe saperlo. Tuttavia, ciò che è vero è che il concetto di blocco asincrono / attende è sicuramente ben collaudato, come SemaphoreSlim.WaitAsyncnel framework .NET. Tutto questo codice fa è aggiungere un concetto di blocco lettore / scrittore.
Contango,

3

Hmm, sembra brutto, sembra funzionare.

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

0

Ho provato ad usare un monitor (codice sotto) che sembra funzionare ma ha un GOTCHA ... quando hai più thread che ti darà ... System.Threading.SynchronizationLockException Il metodo di sincronizzazione degli oggetti è stato chiamato da un blocco di codice non sincronizzato.

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

Prima di questo lo stavo semplicemente facendo, ma era in un controller ASP.NET, quindi si è verificato un deadlock.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.