Wie schreibe ich eine "erwartete" Methode?

70

Ich schaue mir endlich die asynchronen und wartenden Schlüsselwörter an, die ich irgendwie "bekomme", aber alle Beispiele, die ich gesehen habe, rufen asynchrone Methoden im .Net-Framework auf, z. B. dieses , das aufruft HttpClient.GetStringAsync().

Was mir nicht so klar ist, ist, was in einer solchen Methode vor sich geht und wie ich meine eigene "erwartete" Methode schreiben würde. Ist es so einfach, den Code, den ich asynchron in einer Task ausführen möchte, zu verpacken und zurückzugeben?

Andrew Stephens
quelle
2
Oder schreiben Sie eine asynchrone Methode, die natürlich ein Taskoder zurückgibt Task<T>. Async ist auf diese Weise gut zusammensetzbar.
Jon Skeet
2
Ich lerne gerade C #. Ich habe mir das gleiche Beispiel mit GetStringAsync () angesehen und hatte genau die gleiche Frage wie das OP, obwohl ich ein erfahrener Java-Multi-Threader bin. Dies ist eine großartige Frage und sollte wahrscheinlich in dem ansonsten sehr vollständigen MSDN-Artikel behandelt werden.
Pete

Antworten:

86

Es ist so einfach wie

Task.Run(() => ExpensiveTask());

Um es zu einer erwarteten Methode zu machen:

public Task ExpensiveTaskAsync()
{
    return Task.Run(() => ExpensiveTask());
}

Das Wichtigste dabei ist, eine Aufgabe zurückzugeben. Die Methode muss nicht einmal als asynchron markiert werden. (Lesen Sie einfach etwas weiter, damit es ins Bild kommt.)

Dies kann nun als bezeichnet werden

async public void DoStuff()
{
    PrepareExpensiveTask();
    await ExpensiveTaskAsync();
    UseResultsOfExpensiveTask();
}

Beachten Sie, dass hier die Methodensignatur lautet async, da die Methode die Kontrolle bis zur Rückkehr an den Aufrufer ExpensiveTaskAsync()zurückgeben kann. Auch teuer in diesem Fall bedeutet zeitaufwändig, wie eine Webanforderung oder ähnliches. Um umfangreiche Berechnungen an einen anderen Thread zu senden, ist es normalerweise besser, die "alten" Ansätze zu verwenden, dh System.ComponentModel.BackgroundWorkerfür GUI-Anwendungen oder System.Threading.Thread.

Janis F.
quelle
17
Es ist richtig, dass der natürlichste und üblichste Weg, eine erwartete Methode zu erstellen, darin besteht, eine Methode zu schreiben, die Taskoder zurückgibt Task<>. Technisch gesehen können Sie aber auch eine Methode schreiben, die zurückgibt, YourOwnTypesofern YourOwnTypeeine öffentliche, parameterlose, nicht statische Instanzmethode mit dem Namen " GetAwaiter()return return type" vorhanden ist (Details finden Sie an anderer Stelle). So awaitist es ein bisschen wie foreach, es funktioniert auf jedem Typ, der eine geeignete öffentliche Methode hat.
Jeppe Stig Nielsen
1
Das ist gut zu wissen! Obwohl es wahrscheinlich nicht die Lesbarkeit Ihres Codes verbessert, wenn Sie sich für diesen Ansatz entscheiden.
Janis F
2
@JeppeStigNielsen foreachbenötigt IEnumerable. Ich bin ein wenig enttäuscht darüber, dass awaitEnten-Typisierung verwendet wird, wenn eine Benutzeroberfläche für die Sprache besser geeignet wäre. Die Tatsache, dass es "für Compiler" ist, ist eine schlechte Entschuldigung.
Gusdor
2
@ Gusdor Warum, foreachbenötigt keine Schnittstellen! Probieren Sie einfach diese Klassen aus: class ForeachMe { public StrangeType GetEnumerator() { return new StrangeType(); } } class StrangeType { public bool MoveNext() { return true; } public DateTime Current { get { return DateTime.Now; } } }Mit ihnen foreach (var x in new ForeachMe()) { Console.WriteLine(x); }funktioniert der Code einwandfrei.
Jeppe Stig Nielsen
2
@Gusdor Die endgültige Quelle ist der Abschnitt Die foreach-Anweisung (alte Version) in der offiziellen C # -Sprachspezifikation. Finden Sie den gleichen Abschnitt in der neuesten Version . Dies ist gut spezifiziert; Der Typ muss nicht implementiert werden IEnumerableoder IEnumerable<>.
Jeppe Stig Nielsen
15

Wie würde ich meine eigene "erwartete" Methode schreiben? Ist es so einfach, den Code, den ich asynchron ausführen möchte, in einen zu verpacken Taskund diesen zurückzugeben?

Dies ist eine Option, aber höchstwahrscheinlich nicht das, was Sie tun möchten, da Sie dadurch nicht viele der Vorteile von asynchronem Code erhalten. Weitere Informationen finden Sie unter Stephen Toubs Soll ich asynchrone Wrapper für synchrone Methoden verfügbar machen?

Im Allgemeinen sind Methoden nicht zu erwarten, Typen sind es. Wenn Sie in der Lage sein wollen , wie etwas zu schreiben await MyMethod(), dann MyMethod()muss zurückkehren Task, Task<T>oder eine benutzerdefinierte awaitLage Art. Die Verwendung eines benutzerdefinierten Typs ist ein seltenes und fortgeschrittenes Szenario. Mit Taskhaben Sie mehrere Möglichkeiten:

  • Schreiben Sie Ihre Methode mit asyncund await. Dies ist nützlich für Komponieren Aktionen asynchron, aber es kann nicht für den inner meisten verwendet werden awaitkönnen Anrufe.
  • Erstellen Sie die Taskmit einer der Methoden auf Task, wie Task.Run()oder Task.FromAsync().
  • Verwenden Sie TaskCompletionSource. Dies ist der allgemeinste Ansatz. Er kann verwendet werden, um awaitaus allem, was in Zukunft passieren wird, fähige Methoden zu erstellen .
svick
quelle
13

... wie ich meine eigene "erwartbare" Methode schreiben würde.

Die Rückgabe von a Taskist nicht der einzige Weg. Sie haben die Möglichkeit, einen benutzerdefinierten Kellner zu erstellen (indem Sie GetAwaiterund implementieren INotifyCompletion). Hier finden Sie eine gute Lektüre: "Warten Sie auf alles" . Beispiele für .NET-APIs, die benutzerdefinierte Wartende zurückgeben: Task.Yield(), Dispatcher.InvokeAsync.

Ich habe hier und hier einige Posts mit benutzerdefinierten Wartern , z.

// don't use this in production
public static class SwitchContext
{
    public static Awaiter Yield() { return new Awaiter(); }

    public struct Awaiter : System.Runtime.CompilerServices.INotifyCompletion
    {
        public Awaiter GetAwaiter() { return this; }

        public bool IsCompleted { get { return false; } }

        public void OnCompleted(Action continuation)
        {
            ThreadPool.QueueUserWorkItem((state) => ((Action)state)(), continuation);
        }

        public void GetResult() { }
    }
}

// ...

await SwitchContext.Yield();
noseratio
quelle
2
// don't use this in production- warum genau?
Hypersw
@hypersw, hier ist warum .
Noseratio
Ich kann das nicht für ein gutes „Warum“ halten, sondern für ein bestimmtes Detail, das ich in den Links nachverfolgt habe, über awaits, die in finallys nicht verfügbar sind, und die Konsequenzen, die sowieso nicht mehr gelten. Der Rest ist alles sehr spekulativ, wie schlecht aussehender Code ( Task::StartNewjeder Abschnitt statt await YieldTo? Nur wenn Sie es nicht selbst versucht haben) oder semantisch unklar (im Gegensatz zu ConfigureAwait(false), nehme ich an?).
Hypersw
Ich denke, es kommt alles auf Szenarien an. Wenn Sie ein Dutzend Kontextwechsel in einer Funktion haben, hilft dies. Wenn Sie nur große Hintergrundaufgaben starten, wären Sie ohne sicherer.
Hypersw
5

Ja, technisch gesehen müssen Sie nur eine Taskoder Task<Result>von einer asyncMethode zurückgeben, um eine erwartete Methode zu implementieren.

Dies unterstützt das aufgabenbasierte asynchrone Muster .

Es gibt jedoch verschiedene Möglichkeiten, das TAP zu implementieren. Weitere Informationen finden Sie unter Implementieren des aufgabenbasierten asynchronen Musters .

(Aber all diese Implementierungen kehren immer noch zurück Taskoder Task<Result>natürlich.)

Matthew Watson
quelle
5

Konvertieren Sie einfach Ihre Methode in Task. Wie bei @Romiox verwende ich normalerweise diese Erweiterung:

public static partial class Ext
{
    #region Public Methods
    public static Task ToTask(Action action)
    {
        return Task.Run(action);
    }
    public static Task<T> ToTask<T>(Func<T> function)
    {
        return Task.Run(function);
    }
    public static async Task ToTaskAsync(Action action)
    {
        return await Task.Run(action);
    }
    public static async Task<T> ToTaskAsync<T>(Func<T> function)
    {
        return await Task.Run(function);
    }
    #endregion Public Methods
}

Lassen Sie uns jetzt sagen, dass Sie haben

void foo1()...

void foo2(int i1)...

int foo3()...

int foo4(int i1)...

...

Dann können Sie Ihre [asynchrone Methode] wie @Romiox deklarieren

async Task foo1Async()
{
    return await Ext.ToTask(() => foo1());
}
async Task foo2Async(int i1)
{
    return await Ext.ToTask(() => foo2(i1));
}
async Task<int> foo3Async()
{
    return await Ext.ToTask(() => foo3());
}
async Task<int> foo4Async(int i1)
{
    return await Ext.ToTask(() => foo4(i1));
}

ODER

async Task foo1Async()
{
    return await Ext.ToTaskAsync(() => foo1());
}
async Task foo2Async(int i1)
{
    return await Ext.ToTaskAsync(() => foo2(i1));
}
async Task<int> foo3Async()
{
    return await Ext.ToTaskAsync(() => foo3());
}
async Task<int> foo4Async(int i1)
{
    return await Ext.ToTaskAsync(() => foo4(i1));
}

...

Jetzt können Sie async verwenden und auf eine der fooAsync-Methoden warten, z. B. foo4Async

async Task<int> TestAsync () {
    ///Initial Code
    int m = 3;
    ///Call the task
    var X = foo4Async(m);
    ///Between
    ///Do something while waiting comes here
    ///..
    var Result = await X;
    ///Final
    ///Some Code here
    return Result;
}
Waleed AK
quelle
Wo befindet sich das Schlüsselwort await in Ihrem Beispiel?
Jeson Martajaya
2

Wenn Sie a nicht verwenden möchten Task, können Sie ein vollständig angepasstes, zu erwartendes Objekt schreiben. Ein solches Objekt implementiert eine Methode, GetAwaiter ()die ein implementiertes Objekt zurückgibt INotifyCompletion, das das Objekt selbst sein kann.

Mehr: INotifyCompletion

Der Kellner implementiert:

  • IsCompleted ist den Staat zu bekommen
  • GetResult () um das Ergebnis zu erhalten
  • OnCompleted (Action continuation) um den Fortsetzungsdelegierten festzulegen.

Das erwartete Objekt enthält eine Methode für die tatsächliche Nutzlast (z. B. unten ist die Methode Run).

class Program {
    // Need to change the declaration of Main() in order to use 'await'
    static async Task Main () {
        // Create a custom awaitable object
        MyAwaitable awaitable = new MyAwaitable ();

        // Run awaitable payload, ignore returned Task
        _ = awaitable.Run ();

        // Do some other tasks while awaitable is running
        Console.WriteLine ("Waiting for completion...");

        // Wait for completion
        await awaitable;

        Console.WriteLine ("The long operation is now complete. " + awaitable.GetResult());
    }
}

public class MyAwaitable : INotifyCompletion {
    // Fields
    private Action continuation = null;
    private string result = string.Empty;

    // Make this class awaitable
    public MyAwaitable GetAwaiter () { return this; }

    // Implementation of INotifyCompletion for the self-awaiter
    public bool IsCompleted { get; set; }
    public string GetResult () { return result; }
    public void OnCompleted (Action continuation) {
        // Store continuation delegate
        this.continuation = continuation;
        Console.WriteLine ("Continuation set");
    }

    // Payload to run
    public async Task Run () {
        Console.WriteLine ("Computing result...");

        // Wait 2 seconds
        await Task.Delay (2000);
        result = "The result is 10";

        // Set completed
        IsCompleted = true;

        Console.WriteLine ("Result available");

        // Continue with the continuation provided
        continuation?.Invoke ();
    }
}
min
quelle