Wie unterscheidet sich die Verwendung von await von der Verwendung von ContinueWith bei der Verarbeitung von asynchronen Aufgaben?


8

Folgendes meine ich:

public Task<SomeObject> GetSomeObjectByTokenAsync(int id)
    {
        string token = repository.GetTokenById(id);
        if (string.IsNullOrEmpty(token))
        {
            return Task.FromResult(new SomeObject()
            {
                IsAuthorized = false
            });
        }
        else
        {
            return repository.GetSomeObjectByTokenAsync(token).ContinueWith(t =>
            {
                t.Result.IsAuthorized = true;
                return t.Result;
            });
        }
    }

Die obige Methode kann abgewartet werden, und ich denke, sie ähnelt stark dem, was das T- Ask-basierte A- Synchron- P- Muster vorschlägt. (Die anderen mir bekannten Muster sind die APM- und EAP- Muster.)

Was ist nun mit dem folgenden Code:

public async Task<SomeObject> GetSomeObjectByToken(int id)
    {
        string token = repository.GetTokenById(id);
        if (string.IsNullOrEmpty(token))
        {
            return new SomeObject()
            {
                IsAuthorized = false
            };
        }
        else
        {
            SomeObject result = await repository.GetSomeObjectByTokenAsync(token);
            result.IsAuthorized = true;
            return result;
        }
    }

Die Hauptunterschiede hierbei sind, dass die Methode asyncdie awaitSchlüsselwörter verwendet und verwendet. Was ändert sich also im Gegensatz zur zuvor geschriebenen Methode? Ich weiß, dass es auch erwartet werden kann. Jede Methode, die Task zurückgibt, kann dies tun, es sei denn, ich irre mich.

Ich bin mir der Zustandsmaschine bewusst, die mit diesen switch-Anweisungen erstellt wurde, wenn eine Methode als gekennzeichnet ist async, und ich bin mir bewusst, dass sie awaitselbst keinen Thread verwendet - sie blockiert überhaupt nicht, der Thread erledigt einfach andere Dinge, bis dies der Fall ist zurückgerufen, um die Ausführung des obigen Codes fortzusetzen.

Aber was ist der zugrunde liegende Unterschied zwischen den beiden Methoden, wenn wir sie mit dem awaitSchlüsselwort aufrufen ? Gibt es überhaupt einen Unterschied, und wenn ja, welcher wird bevorzugt?

BEARBEITEN: Ich bin der Meinung, dass das erste Code-Snippet bevorzugt wird, da wir die Schlüsselwörter async / await effektiv eliminieren , ohne dass dies Auswirkungen hat. Wir geben eine Aufgabe zurück, die ihre Ausführung synchron fortsetzt, oder eine bereits abgeschlossene Aufgabe auf dem Hot Path (was sein kann) zwischengespeichert).


3
In diesem Fall sehr wenig. In Ihrem ersten Beispiel result.IsAuthorized = truewird es im Thread-Pool ausgeführt, während es im zweiten Beispiel möglicherweise im selben Thread ausgeführt wird, der aufgerufen wurde GetSomeObjectByToken(wenn ein Thread darauf SynchronizationContextinstalliert war, z. B. ein UI-Thread). Das Verhalten beim GetSomeObjectByTokenAsyncAuslösen einer Ausnahme unterscheidet sich ebenfalls geringfügig. Im Allgemeinen awaitwird es vorgezogen ContinueWith, da es fast immer besser lesbar ist.
Kanton7

1
In diesem Artikel heißt es "eliding", was ein ziemlich gutes Wort dafür zu sein scheint. Stephen kennt sich definitiv aus. Die Schlussfolgerung des Artikels ist im Wesentlichen, dass es in Bezug auf die Leistung nicht viel ausmacht, aber es gibt bestimmte Codierungsprobleme, wenn Sie nicht warten. Ihr zweites Beispiel ist ein sichereres Muster.
John Wu

1
Diese Twitter-Diskussion des Partner Software Architect von Microsoft im ASP.NET-Team könnte Sie interessieren: twitter.com/davidfowl/status/1044847039929028608?lang=de
Remy

Es wird eine "Zustandsmaschine" genannt, keine "virtuelle Maschine".
Rätselhaftigkeit

@Enigmativity Danke, ich habe vergessen, das zu bearbeiten!
SpiritBob

Antworten:


5

Mit dem Mechanismus async/ awaitwandelt der Compiler Ihren Code in eine Zustandsmaschine um. Ihr Code wird synchron ausgeführt, bis der erste Code awaitauf einen Wartenden trifft, der gegebenenfalls noch nicht abgeschlossen ist.

Im Microsoft C # -Compiler handelt es sich bei dieser Zustandsmaschine um einen awaitWerttyp. Dies bedeutet, dass sie sehr geringe Kosten verursacht, wenn alle zu erwartenden Aufgaben abgeschlossen sind, da sie kein Objekt zuweist und daher keinen Müll generiert. Wenn eine Wartezeit nicht abgeschlossen ist, wird dieser Werttyp unweigerlich eingerahmt.

Beachten Sie, dass dies die Zuweisung von Tasks nicht vermeidet, wenn dies die Art der in den awaitAusdrücken verwendeten Wartezeiten ist .

Mit ContinueWithvermeiden Sie Zuordnungen (außer Task) nur, wenn Ihre Fortsetzung keinen Abschluss hat und wenn Sie entweder kein Statusobjekt verwenden oder ein Statusobjekt so oft wie möglich wiederverwenden (z. B. aus einem Pool).

Außerdem wird die Fortsetzung aufgerufen, wenn die Aufgabe abgeschlossen ist und ein Stapelrahmen erstellt wird. Sie wird nicht inline. Das Framework versucht, Stapelüberläufe zu vermeiden, aber es kann vorkommen, dass ein Stapelüberlauf nicht vermieden wird, z. B. wenn große Arrays stapelweise zugewiesen werden.

Dies wird vermieden, indem überprüft wird, wie viel Stapel noch vorhanden ist. Wenn der Stapel durch eine interne Maßnahme als voll angesehen wird, wird die Fortsetzung im Taskplaner geplant. Es wird versucht, schwerwiegende Ausnahmen für Stapelüberläufe auf Kosten der Leistung zu vermeiden.

Hier ist ein subtiler Unterschied zwischen async/ awaitund ContinueWith:

  • asyncIch awaitwerde Fortsetzungen SynchronizationContext.Currenteinplanen, falls vorhanden, andernfalls in TaskScheduler.Current 1

  • ContinueWithplant Fortsetzungen im bereitgestellten Taskplaner oder TaskScheduler.Currentin den Überladungen ohne den Parameter Taskplaner

So simulieren Sie das Standardverhalten von async/ await:

.ContinueWith(continuationAction,
    SynchronizationContext.Current != null ?
        TaskScheduler.FromCurrentSynchronizationContext() :
        TaskScheduler.Current)

Zur Simulation async/ await‚s Verhalten Task‘ s .ConfigureAwait(false):

.ContinueWith(continuationAction,
    TaskScheduler.Default)

Mit Schleifen und Ausnahmebehandlung wird es immer komplizierter. Abgesehen davon, dass Ihr Code lesbar bleibt , funktioniert async/ awaitmit allen erwarteten .

Ihr Fall wird am besten mit einem gemischten Ansatz behandelt: einer synchronen Methode, die bei Bedarf eine asynchrone Methode aufruft. Ein Beispiel für Ihren Code mit diesem Ansatz:

public Task<SomeObject> GetSomeObjectByTokenAsync(int id)
{
    string token = repository.GetTokenById(id);
    if (string.IsNullOrEmpty(token))
    {
        return Task.FromResult(new SomeObject()
        {
            IsAuthorized = false
        });
    }
    else
    {
        return InternalGetSomeObjectByTokenAsync(repository, token);
    }
}

internal async Task<SomeObject> InternalGetSomeObjectByToken(Repository repository, string token)
{
    SomeObject result = await repository.GetSomeObjectByTokenAsync(token);
    result.IsAuthorized = true;
    return result;
}

Nach meiner Erfahrung habe ich nur sehr wenige Stellen im Anwendungscode gefunden , an denen sich das Hinzufügen einer solchen Komplexität tatsächlich auszahlt, um solche Ansätze zu entwickeln, zu überprüfen und zu testen, während im Bibliothekscode jede Methode ein Engpass sein kann.

Der einzige Fall, in dem ich dazu neige, Aufgaben zu erledigen, ist, wenn eine Taskoder eine Task<T>zurückgegebene Methode einfach das Ergebnis einer anderen asynchronen Methode zurückgibt, ohne selbst eine E / A oder eine Nachbearbeitung durchgeführt zu haben.

YMMV.


  1. Es sei denn, Sie verwenden ConfigureAwait(false)oder warten auf eine Wartezeit, die benutzerdefinierte Zeitplanung verwendet

1
Die Zustandsmaschine ist kein Werttyp. Schauen Sie sich die generierte Quelle eines einfachen asynchronen Programms an : private sealed class <Main>d__0 : IAsyncStateMachine. Die Instanzen dieser Klasse können jedoch wiederverwendbar sein.
Theodor Zoulias

Würden Sie sagen, wenn ich nur das Ergebnis einer Aufgabe manipulieren würde, wäre es in Ordnung, sie zu verwenden ContinueWith?
SpiritBob

3
@TheodorZoulias, die generierte Quell- Targeting-Version anstelle von Debug generiert Strukturen.
Acelent

2
@SpiritBob, wenn Sie wirklich mit Aufgabenergebnissen herumspielen möchten, denke ich, dass es in Ordnung ist, .ContinueWithsollte programmgesteuert verwendet werden. Besonders dann, wenn Sie CPU-intensive Aufgaben anstelle von E / A-Aufgaben erledigen. Wenn Sie sich jedoch mit den TaskEigenschaften, der AggregateExceptionKomplexität und den Komplexitätsbedingungen, Zyklen und Ausnahmebehandlungen befassen müssen, wenn weitere asynchrone Methoden aufgerufen werden, gibt es kaum einen Grund, sich daran zu halten.
Acelent

2
@SpiritBob, sorry, ich meinte "Schleifen". Ja, Sie können ein statisches schreibgeschütztes Feld mit a erstellen Task.FromResult("..."). Im asynchronen Code, wenn Sie Werte von Schlüssel - Cache , die I / O benötigen zu erhalten, können Sie ein Wörterbuch verwenden , bei denen die Werte Aufgaben, zB ConcurrentDictionary<string, Task<T>>statt ConcurrentDictionary<string, T>, Verwendung GetOrAddAnruf mit einer Fabrik Funktion und warten auf das Ergebnis. Dies garantiert, dass nur eine E / A-Anforderung zum Auffüllen des Cache-Schlüssels gestellt wird. Sie weckt die Wartenden, sobald die Aufgabe abgeschlossen ist, und dient anschließend als abgeschlossene Aufgabe.
Acelent

2

Durch die Verwendung verwenden ContinueWithSie die Tools, die vor der Einführung der async/ await-Funktionalität mit C # 5 im Jahr 2012 verfügbar waren . Als Tool ist es ausführlich, nicht einfach zusammenzusetzen und erfordert zusätzliche Arbeit zum Auspacken von AggregateExceptions und Task<Task<TResult>>Rückgabewerten (diese erhalten Sie, wenn Sie übergeben asynchrone Delegaten als Argumente. Im Gegenzug bietet es nur wenige Vorteile. Sie können es verwenden, wenn Sie mehrere Fortsetzungen an dieselbe anhängen möchten Taskoder in einigen seltenen Fällen, in denen Sie async/ awaitaus irgendeinem Grund nicht verwenden können (z. B. wenn Sie sich in einer Methode mit outParametern befinden ).


Update: Ich habe den irreführenden Rat entfernt, den ContinueWithdas verwenden sollte TaskScheduler.Default, um das Standardverhalten von nachzuahmen await. Eigentlichawait plant der standardmäßig seine Fortsetzung mit TaskScheduler.Current.

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.