Warum kann ich den Operator 'await' nicht im Hauptteil einer lock-Anweisung verwenden?


348

Das Schlüsselwort await in C # (.NET Async CTP) ist in einer lock-Anweisung nicht zulässig.

Von MSDN :

Ein Warten-Ausdruck kann nicht in einer synchronen Funktion, in einem Abfrageausdruck, im catch- oder finally-Block einer Ausnahmebehandlungsanweisung, im Block einer lock-Anweisung oder in einem unsicheren Kontext verwendet werden.

Ich gehe davon aus, dass dies für das Compilerteam aus irgendeinem Grund entweder schwierig oder unmöglich zu implementieren ist.

Ich habe versucht, die using-Anweisung zu umgehen:

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

Dies funktioniert jedoch nicht wie erwartet. Der Aufruf von Monitor.Exit in ExitDisposable.Dispose scheint auf unbestimmte Zeit (meistens) zu blockieren, was zu Deadlocks führt, wenn andere Threads versuchen, die Sperre zu erlangen. Ich vermute, dass die Unzuverlässigkeit meiner Arbeit und der Grund, warum Warten-Anweisungen in Lock-Anweisungen nicht zulässig sind, irgendwie zusammenhängen.

Weiß jemand, warum das Warten im Text einer Lock-Anweisung nicht erlaubt ist?


27
Ich würde mir vorstellen, dass Sie den Grund gefunden haben, warum dies nicht erlaubt ist.
Asawyer


Ich fange gerade an aufzuholen und etwas mehr über asynchrone Programmierung zu lernen. Nach zahlreichen Deadlocks in meinen wpf-Anwendungen stellte ich fest, dass dieser Artikel ein hervorragender Schutz für asynchrone Programmierpraktiken ist. msdn.microsoft.com/en-us/magazine/…
C. Tewalt

Die Sperre soll den asynchronen Zugriff verhindern, wenn der asynchrone Zugriff Ihren Code beschädigen würde. Wenn Sie also asynchron in einem Schloss verwenden, haben Sie Ihr Schloss ungültig gemacht. Wenn Sie also auf etwas in Ihrem Schloss warten müssen, verwenden Sie das Schloss nicht richtig
MikeT

Antworten:


366

Ich gehe davon aus, dass dies für das Compilerteam aus irgendeinem Grund entweder schwierig oder unmöglich zu implementieren ist.

Nein, die Implementierung ist überhaupt nicht schwierig oder unmöglich - die Tatsache, dass Sie sie selbst implementiert haben, ist ein Beweis dafür. Vielmehr ist es eine unglaublich schlechte Idee , und so haben wir es nicht zulassen, so wie Sie zu schützen , diesen Fehler zu machen.

Aufruf von Monitor.Exit innerhalb von ExitDisposable.Dispose scheint auf unbestimmte Zeit (meistens) zu blockieren, was zu Deadlocks führt, wenn andere Threads versuchen, die Sperre zu erlangen. Ich vermute, dass die Unzuverlässigkeit meiner Arbeit und der Grund, warum Warten auf Anweisungen in Lock-Anweisungen nicht zulässig sind, irgendwie zusammenhängen.

Richtig, Sie haben herausgefunden, warum wir es illegal gemacht haben. In einem Schloss wartet ein Rezept für die Herstellung von Deadlocks.

Ich bin sicher, Sie können verstehen, warum: Zwischen dem Zeitpunkt, an dem das Warten die Steuerung an den Aufrufer zurückgibt, und der Wiederaufnahme der Methode wird beliebiger Code ausgeführt . Dieser willkürliche Code könnte Sperren entfernen, die Inversionen der Sperrenreihenfolge und damit Deadlocks erzeugen.

Schlimmer noch, der Code könnte in einem anderen Thread fortgesetzt werden (in fortgeschrittenen Szenarien; normalerweise wird der Thread, der gewartet hat, aber nicht unbedingt wieder aufgenommen). In diesem Fall würde das Entsperren eine Sperre für einen anderen Thread als den Thread entsperren, der benötigt wurde aus dem Schloss. Ist das eine gute Idee? Nein.

Ich stelle fest, dass es aus dem gleichen Grund auch eine "schlimmste Praxis" ist, ein yield returnInside a zu machen lock. Es ist legal, dies zu tun, aber ich wünschte, wir hätten es illegal gemacht. Wir werden nicht den gleichen Fehler für "Warten" machen.


188
Wie gehen Sie mit einem Szenario um, in dem Sie einen Cache-Eintrag zurückgeben müssen, und wenn der Eintrag nicht vorhanden ist, müssen Sie den Inhalt asynchron berechnen und dann den Eintrag hinzufügen + zurückgeben, um sicherzustellen, dass Sie in der Zwischenzeit von niemand anderem angerufen werden?
Softlion

9
Mir ist klar, dass ich zu spät zur Party komme, aber ich war überrascht zu sehen, dass Sie Deadlocks als Hauptgrund dafür angeführt haben, warum dies eine schlechte Idee ist. Ich war in meinem eigenen Denken zu dem Schluss gekommen, dass der Wiedereintritt von Schloss / Monitor ein größerer Teil des Problems sein würde. Das heißt, Sie stellen zwei Aufgaben in den Thread-Pool, die lock () sind und die in einer synchronen Welt auf separaten Threads ausgeführt werden. Aber jetzt mit Warten (wenn erlaubt, meine ich) könnten zwei Aufgaben innerhalb des Sperrblocks ausgeführt werden, da der Thread wiederverwendet wurde. Es kommt zu Heiterkeit. Oder habe ich etwas falsch verstanden?
Gareth Wilson

4
@GarethWilson: Ich habe über Deadlocks gesprochen, weil die gestellte Frage Deadlocks betraf . Sie haben Recht, dass bizarre Wiedereintrittsprobleme möglich sind und wahrscheinlich erscheinen.
Eric Lippert

11
@ Eric Lippert. Angesichts der Tatsache, dass die SemaphoreSlim.WaitAsyncKlasse dem .NET Framework hinzugefügt wurde, nachdem Sie diese Antwort veröffentlicht haben, können wir davon ausgehen, dass dies jetzt möglich ist. Unabhängig davon sind Ihre Kommentare zu den Schwierigkeiten bei der Implementierung eines solchen Konstrukts weiterhin uneingeschränkt gültig.
Contango

7
"Beliebiger Code wird zwischen dem Zeitpunkt ausgeführt, an dem das Warten die Steuerung an den Aufrufer zurückgibt und die Methode fortgesetzt wird" - dies gilt sicherlich für jeden Code, auch wenn kein asynchrones / Warten vorhanden ist, in einem Multithread-Kontext: Andere Threads können beliebigen Code ausführen Zeit, und der beliebige Code, wie Sie sagen, "könnte Sperren entfernen, die Inversionen der Sperrenreihenfolge und damit Deadlocks erzeugen." Warum ist dies bei async / await von besonderer Bedeutung? Ich verstehe den zweiten Punkt bezüglich "Der Code könnte in einem anderen Thread wieder aufgenommen werden" von besonderer Bedeutung für Async / Warten.
Bacar

291

Verwenden Sie die SemaphoreSlim.WaitAsyncMethode.

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

10
Da diese Methode kürzlich in das .NET Framework eingeführt wurde, können wir davon ausgehen, dass das Konzept des Sperren in einer asynchronen / wartenden Welt mittlerweile gut bewiesen ist.
Contango

5
Für weitere Informationen suchen Sie nach dem Text "SemaphoreSlim" in diesem Artikel: Async / Await - Best Practices in der asynchronen Programmierung
BobbyA

1
@ JamesKo, wenn all diese Aufgaben auf das Ergebnis warten, Stuffsehe ich keinen Weg daran vorbei ...
Ohad Schneider

7
Sollte es nicht so initialisiert werden, wie mySemaphoreSlim = new SemaphoreSlim(1, 1)es funktioniert lock(...)?
Sergey

3
Die erweiterte Version dieser Antwort wurde hinzugefügt
Sergey

67

Grundsätzlich wäre es das Falsche.

Dies kann auf zwei Arten implementiert werden:

  • Halten Sie das Schloss fest und lassen Sie es erst am Ende des Blocks los .
    Dies ist eine wirklich schlechte Idee, da Sie nicht wissen, wie lange der asynchrone Vorgang dauern wird. Sie sollten Sperren nur minimal halten Zeit halten. Dies ist möglicherweise auch unmöglich, da ein Thread eine Sperre und keine Methode besitzt - und Sie möglicherweise nicht einmal den Rest der asynchronen Methode auf demselben Thread ausführen (abhängig vom Taskplaner).

  • Lassen Sie die Sperre in der Wartezeit los und fordern Sie sie erneut an, wenn die Wartezeit zurückkehrt
    Dies verstößt gegen das Prinzip der IMO mit dem geringsten Erstaunen, bei dem sich die asynchrone Methode so genau wie möglich wie der entsprechende synchrone Code verhalten sollte - es sei denn, Sie verwenden sie Monitor.Waitin einem Sperrblock Besitzen Sie die Sperre für die Dauer des Blocks.

Grundsätzlich gibt es hier zwei konkurrierende Anforderungen - das sollten Sie nicht sein versuchen , die erste zu tun, und wenn Sie den zweiten Ansatz wählen möchten, können Sie den Code viel klarer machen, indem Sie zwei getrennte Sperrblöcke durch den Ausdruck wait trennen:

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

Indem die Sprache Ihnen verbietet, im Sperrblock selbst zu warten, zwingt Sie die Sprache dazu, darüber nachzudenken, was Sie wirklich tun möchten, und macht diese Auswahl in dem Code, den Sie schreiben, klarer.


5
Angesichts der Tatsache, dass die SemaphoreSlim.WaitAsyncKlasse dem .NET Framework hinzugefügt wurde, nachdem Sie diese Antwort veröffentlicht haben, können wir davon ausgehen, dass dies jetzt möglich ist. Unabhängig davon sind Ihre Kommentare zu den Schwierigkeiten bei der Implementierung eines solchen Konstrukts weiterhin uneingeschränkt gültig.
Contango

7
@ Contango: Nun, das ist nicht ganz dasselbe. Insbesondere ist das Semaphor nicht an einen bestimmten Thread gebunden. Es erreicht ähnliche Ziele zu sperren, aber es gibt signifikante Unterschiede.
Jon Skeet

@ JonSkeet Ich weiß, dass dies ein sehr alter Thread ist, aber ich bin nicht sicher, wie der Aufruf von Something () durch diese Sperren auf die zweite Weise geschützt wird. Wenn ein Thread etwas ausführt (), kann auch jeder andere Thread daran beteiligt sein! Vermisse ich hier etwas?

@ Joseph: Es ist zu diesem Zeitpunkt nicht geschützt. Es ist der zweite Ansatz, der deutlich macht, dass Sie erwerben / freigeben und dann erneut erwerben / freigeben, möglicherweise in einem anderen Thread. Weil der erste Ansatz laut Erics Antwort eine schlechte Idee ist.
Jon Skeet

41

Dies ist nur eine Erweiterung dieser Antwort .

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

Verwendungszweck:

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
Es kann gefährlich sein, die Semaphorsperre außerhalb des tryBlocks zu erhalten - wenn eine Ausnahme zwischen auftritt WaitAsyncund trydas Semaphor niemals freigegeben wird (Deadlock). Auf der anderen Seite führt das Verschieben eines WaitAsyncAnrufs in den tryBlock zu einem weiteren Problem, wenn das Semaphor freigegeben werden kann, ohne dass eine Sperre erworben wird. Siehe den zugehörigen Thread, in dem dieses Problem erklärt wurde: stackoverflow.com/a/61806749/7889645
AndreyCh

16

Dies bezieht sich auf http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx , http://winrtstoragehelper.codeplex.com/ , Windows 8 App Store und .net 4.5

Hier ist mein Blickwinkel dazu:

Die Sprachfunktion async / await macht viele Dinge ziemlich einfach, führt aber auch ein Szenario ein, das selten vor der Verwendung von asynchronen Aufrufen aufgetreten ist: Wiedereintritt.

Dies gilt insbesondere für Ereignishandler, da Sie für viele Ereignisse keine Ahnung haben, was passiert, nachdem Sie vom Ereignishandler zurückgekehrt sind. Eine Sache, die tatsächlich passieren könnte, ist, dass die asynchrone Methode, auf die Sie im ersten Ereignishandler warten, von einem anderen Ereignishandler aufgerufen wird, der sich noch im selben Thread befindet.

Hier ist ein reales Szenario, auf das ich in einer Windows 8 App Store-App gestoßen bin: Meine App hat zwei Frames: Ein- und Aussteigen aus einem Frame, in den ich einige Daten in eine Datei / einen Speicher laden / sichern möchte. OnNavigatedTo / From-Ereignisse werden zum Speichern und Laden verwendet. Das Speichern und Laden erfolgt über eine asynchrone Dienstprogrammfunktion (wie http://winrtstoragehelper.codeplex.com/ ). Beim Navigieren von Frame 1 zu Frame 2 oder in die andere Richtung werden die asynchrone Last und die sicheren Operationen aufgerufen und abgewartet. Die Event-Handler werden asynchron und geben void zurück => Sie können nicht erwartet werden.

Die erste Dateiöffnungsoperation (sagen wir: innerhalb einer Speicherfunktion) des Dienstprogramms ist jedoch ebenfalls asynchron, und daher gibt die erste Wartezeit die Steuerung an das Framework zurück, das einige Zeit später das andere Dienstprogramm (Laden) über den zweiten Ereignishandler aufruft. Das Laden versucht nun, dieselbe Datei zu öffnen. Wenn die Datei jetzt für den Speichervorgang geöffnet ist, schlägt dies mit einer ACCESSDENIED-Ausnahme fehl.

Eine Mindestlösung für mich besteht darin, den Dateizugriff über ein using und ein AsyncLock zu sichern.

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

Bitte beachten Sie, dass seine Sperre grundsätzlich alle Dateivorgänge für das Dienstprogramm mit nur einer Sperre sperrt, was unnötig stark ist, aber für mein Szenario gut funktioniert.

Hier ist mein Testprojekt: Eine Windows 8 App Store-App mit einigen Testaufrufen für die Originalversion von http://winrtstoragehelper.codeplex.com/ und meine modifizierte Version, die AsyncLock von Stephen Toub http: //blogs.msdn verwendet. com / b / pfxteam / archive / 2012/02/12/10266988.aspx .

Darf ich auch diesen Link vorschlagen: http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx


7

Stephen Taub hat eine Lösung für diese Frage implementiert, siehe Erstellen von Grundelementen für die asynchrone Koordination, Teil 7: AsyncReaderWriterLock .

Stephen Taub genießt in der Branche hohes Ansehen, daher ist alles, was er schreibt, wahrscheinlich solide.

Ich werde den Code, den er in seinem Blog gepostet hat, nicht reproduzieren, aber ich werde Ihnen zeigen, wie man ihn verwendet:

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

Wenn Sie eine Methode möchten, die in das .NET Framework integriert ist, verwenden Sie SemaphoreSlim.WaitAsyncstattdessen. Sie erhalten keine Lese- / Schreibsperre, aber Sie erhalten eine bewährte Implementierung.


Ich bin gespannt, ob die Verwendung dieses Codes Vorbehalte aufweist. Wenn jemand Probleme mit diesem Code nachweisen kann, würde ich gerne wissen. Was jedoch stimmt, ist, dass das Konzept der asynchronen / wartenden Sperre definitiv gut bewiesen ist, wie SemaphoreSlim.WaitAsynces im .NET Framework der Fall ist. Dieser Code fügt lediglich ein Lese- / Schreibsperrkonzept hinzu.
Contango

3

Hmm, sieht hässlich aus, scheint zu funktionieren.

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

Ich habe versucht, einen Monitor (Code unten) zu verwenden, der zu funktionieren scheint, aber ein GOTCHA hat ... wenn Sie mehrere Threads haben, wird es ... System.Threading.SynchronizationLockException Die Objektsynchronisationsmethode wurde aus einem nicht synchronisierten Codeblock aufgerufen.

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

Vorher habe ich das einfach gemacht, aber es war in einem ASP.NET-Controller, so dass es zu einem Deadlock kam.

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

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.