Laden...

Best practice: Methode, die mehrere asynchrone (oder doch besser synchrone?) Methoden aufruft

Erstellt von BlackMatrix vor 9 Jahren Letzter Beitrag vor 9 Jahren 2.407 Views
B
BlackMatrix Themenstarter:in
218 Beiträge seit 2012
vor 9 Jahren
Best practice: Methode, die mehrere asynchrone (oder doch besser synchrone?) Methoden aufruft

Ich schreibe eine API, die verschiedene Methoden asynchron anbietet. Da meine Methoden Anfragen über das Netzwerk starten, handelt es sich um externe Nebenläufigkeit wie in Erstellen einer entsprechenden sync Funktion für eine async Funktion (oder besser andersherum?) erklärt.

Nun gibt es aber noch einige Methoden in der API, die verschiedene Methoden zusammenfassen. Der Code würde dann einfach nur wie folgt aussehen:


        public async Task DoFullJob()
        {
            await InitializeAsync();
            await LoginAsync();
            await StartAsync();
            await UninitializeAsync();
        }

Soweit mir bekannt ist, wird dann ja jedes Mal ein neues Taskobjekt angelegt, sollte man demnach besser synchrone Methoden erstellen und das ganze so kapseln, dass nur ein Task erstellt wird und die Ausführung ja sowieso nacheinander erfolgt?


        public Task DoFullJob()
        {
            return Task.Run(() =>
            {
                Initialize();
                Login();
                Start();
                Uninitialize();
            });
        }

Viele Grüße

BlackMatrix

2.079 Beiträge seit 2012
vor 9 Jahren

Die zweite Variante würde bedeuten, dass alle vier Methode parallel ausgeführt werden.
Das await sagt nur, dass auf diese Methoden gewartet wird, lässt du das weg, laufen sie asynchron.

Neue Tasks werden sowieso erstellt, da jede der asynchronen Methoden, auf die man warten kann, ein Task-Objekt zurück geben muss.

Ob die vielen Tasks einen Unterschied machen, kann ich dir allerdings nicht sagen.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

16.832 Beiträge seit 2008
vor 9 Jahren

Die Frage ist: wieso gibt es Initialize-Methoden bzw. wieso muss sich der API-Konsument um die Reihenfolge kümmern?
Erscheint mir nicht durchdacht. Erklärt mal den Kontext.

2.079 Beiträge seit 2012
vor 9 Jahren

Da wäre es vielleicht tatsächlich besser, diese vier Methoden (private oder public) synchron anzubieten und dann in einem Task auszuführen. Das wäre dann der zweiten Variante entsprechen, nur die Methode als async markiert und anstelle des return ein await.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

6.911 Beiträge seit 2009
vor 9 Jahren

Hallo Palladin007,

deine Antwort ist leider nicht ganz korrekt.

Die zweite Variante würde bedeuten, dass alle vier Methode parallel ausgeführt werden.

Warum? Task.Run wird eine anonyme Methode übergeben in welcher Initialize, Login, etc. synchron ausgeführt werden - unter der Annahme dass sich BlackMatrix an die Namenskonvention gehalten hat wovon hier auszugehen ist.

Das await sagt nur, dass auf diese Methoden gewartet wird

"Warten" ist in Bezug auf async/await mit Vorsicht zu genießen. Korrekterweise wird durch await dem Compiler nur mitgeteilt die Methode umzuschreiben und zwar so, dass der Rest nach await als Continuation angehängt wird, welche ausgeführt wird (bzw. exakter: dem jeweiligen Scheduler übergeben wird) wenn das "Awaitable" fertig ist.

da jede der asynchronen Methoden, auf die man warten kann, ein Task-Objekt zurück geben muss.

Es muss nicht eine Task-Objekt sein. Siehe await anything;

Hallo BlackMatrix,

wenn deine Methoden über das Netzwerk gehen so spielen IO-Abschlussthreads eine Rolle und in Bezug auf Skalierbarkeit wäre es demnach vermutlich besser die 1. Variante zu verwenden. Durch die mehreren Task-Objekte hat zwar der GC etwas mehr arbeit, aber ich bin mir fast sicher dass dies hier (u.v.a. bei Task-Objekten) nicht recht ins Gewicht fallen wird.
Außerdem kann bei der 1. Variante der Task-Scheduler "besser" arbeiten, da i.d.R. viele kleine Tasks besser sind als wenige große.

Wie Abt schon korrekt einwendet sollte das aber intern passieren und der API-Konsument nur eine Methode angeboten bekommen - je nach Kontext halt 😉

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

742 Beiträge seit 2005
vor 9 Jahren

Du kannst doch auch einfach eine asynchrone Methode anbieten, die intern mit async und await arbeitet:


public async Task LoginAnDoAsync()
{
 await InitializeAsync();
 await LoginAsync();
 await StartAsync();
 await UninitializeAsync();
}

Man kann bei sowas noch Performance sparen, indem man die Verkettung von Tasks einfach selber macht:


public async Task LoginAnDoAsync()
{
    return InitializeAsync()
        .ContinueWith(x => LoginAsync())
        .ContinueWith(x => StartAsync())
        .ContinueWith(x => UninitializeAsync());
}

Der Compiler muss nun keine State-Machine mehr generieren.

Man muss aber aufpassen, weil dadurch ein Teil der Arbeit wieder in einen deiner Haupt-Threads verlagert wird, zum Beispiel der UI-Thread.

49.485 Beiträge seit 2005
vor 9 Jahren

Hallo malignate,

ob man da wirklich (nennenswert) Performance sparen kann, halte ich für fraglich. Die Statemashine wird ja vom Compiler, also zur Compilezeit, generiert und zur Laufzeit einfach nur ausgeführt. Das kostet für sich genommen so gut wie keinen Aufwand.

Allerdings muss sich man - so oder so - darüber Gedanken machen, in welchen Threads die Tasks laufen. Das hängt aber nicht nur von der Methode selbst ab, sondern auch davon, aus welchen Thread heraus sie aufgerufen wird. In diesem Zusammenhang, sollte man noch auf ConfigureAwait(false) hinweisen. Doch die volle Kontrolle hat man m.E. nur bei synchronen Methoden.

Außerdem würde ich vermuten, dass in deinem zweiten Vorschlag die Methoden teilweise echt parallel ablaufen, was wohl nicht gewünscht ist. Denn die zweite (und dritte) Continuation kann und wird starten, sobald die in der vorherigen Continuation ausgerufene Methode zurückkehrt und das ist bei Async-Methode in der Regel der Fall, bevor die durchzuführende eigentliche Aktion abgeschlossen ist. Oder übersehe ich hier etwas?

herbivore

849 Beiträge seit 2006
vor 9 Jahren

Hallo Herbivore,

das ist meiner Meinung nicht ganz richtig. Die Methoden werden imho nacheinander abgearbeitet.

Wenn Du folgenden Testcode her nimmst dürfte o in der Machwas(object o) Methode in deiner Version nicht gefüllt sein. Was er aber ist. Somit wird der Thread der TestMethode wohl solange Schlafen gelegt bis die erste Methode zurück ist.


static void Main(string[] args)
		{
			Test();
			Console.ReadLine();
		}

		async static void Test()
		{
			var x = await Machwas();
			var y = await Machwas(x);
		}

		public static async Task<object>  Machwas()
		{
			await Task.Delay(1000);

			return new object();
		}

		public static async Task<object> Machwas(object o)
		{
			await Task.Delay(1000);

			return new object();
		}


742 Beiträge seit 2005
vor 9 Jahren

@Herbivore:

49.485 Beiträge seit 2005
vor 9 Jahren

Hallo unconnected,

Die Methoden werden imho nacheinander abgearbeitet.

du verwendet in deinem Beispiel an der Aufrufstelle await. Dadurch werden die Aktionen nacheinander ausgeführt. Mein Einwand bezog sich auf das zweite Beispiel von malignate. Und dort sehe ich meine Aussage, dass die durch die Continuations angestoßenen, eigentlichen Aufgaben parallel ausgeführt werden, noch nicht widerlegt.

herbivore

742 Beiträge seit 2005
vor 9 Jahren

@Herbivore:

Man kann ContinueWith ja einfach hintereinanderkettern, da ContinueWith ein Task zurückliefert, der die angehängte Aufgabe repräsentiert.


static void Main(string[] args)
{
    string result =
        Task.Run<int>(() => Test1())
            .ContinueWith<int>(x => Test2(x.Result))
            .ContinueWith<string>(x => Test3(x.Result)).Result;

    Console.WriteLine(result);
    Console.Read();
}

public static int Test1()
{
    return 1;
}

public static int Test2(int x)
{
    return x + 1;
}

public static string Test3(int x)
{
    return "Hello" + x;
}

Das liefert nämlich das gewünschte Ergebnis: Nämlich Hello2

Da aber in unserem Async-Szenario ein im ContinueWith wieder ein neuer Task erstellt wird, müssen wir aus einem Task<Task> ein Task erstellen, damit wir mit dem weiteren Ablauf fortfahrten können, wenn der innere Task fertig ist und nicht der äußere. Die Implementierung ist ein bisschen tricky, in Mono ist es aber einfacher zu verstehen:

github / mono / mcs / class / corlib / System.Threading.Tasks / TaskExtensionsImpl.cs

Wahrscheinlich ist hier aber eine State-Machine sogar einfacher 😉

849 Beiträge seit 2006
vor 9 Jahren

@ Herbivore,

stimmt da hatte ich wohl ein wenig schnell & falsch gelesen, aber trotzdem hätte ich erwartet das continuewith wartet. Andererseits ist es auch wieder Logisch das es genau das nicht tut.