Laden...

Macht es einen Unterschied, ob ich einen Task await oder durchroute, wenn der caller await'ed?

Erstellt von lutzeslife vor 3 Jahren Letzter Beitrag vor 3 Jahren 2.071 Views
L
lutzeslife Themenstarter:in
155 Beiträge seit 2012
vor 3 Jahren
Macht es einen Unterschied, ob ich einen Task await oder durchroute, wenn der caller await'ed?

Hallo Community,

ich habe eine Frage zum Thema Task bzw. async/await.


        static async void Main(string[] args)
        {
            await DoSomthingAsync();

            await DoSomthingAsync2();
        }


        static async Task DoSomthingAsync()
        {
            await Calling();
        }

        static Task DoSomthingAsync2()
        {
            return Calling();
        }


        static Task Calling() => Task.Delay(1);

Meine Frage ist ob es einen Unterschied macht, ob ich auf den Task awaite wie in DoSomthingAsync oder den Task durchroute wenn der Aufrufer sowieso ein await macht?

Das einzige was mir auffällt ist dass wenn ich schon ein await mache:


        static async Task DoSomthingAsync3()
        {
            await Calling();
            return Calling();
        }

Das ich dann den letzten Aufruf durchrouten kann.

Mit freundlichen Grüßen
lutzeslife

16.806 Beiträge seit 2008
vor 3 Jahren

Es ist prinzipiell das gleiche, mit der Ausnahme dass ein "leeres Await" Compiler Overhead in der StateMachine erzeugt; der Overhead ist aber marginal und ist eben ein technischer Overhead.

Daher, wenn Du nur einen await in einer Methode hast, await wegnehmen und den Task returnen.

PS: das steht auch in den Microsoft Task Docs IIRC.

L
lutzeslife Themenstarter:in
155 Beiträge seit 2012
vor 3 Jahren

Danke Abt,

Welches Dokument meinst du mit IIRC?

Mit freundlichen Grüßen
lutzeslife

16.806 Beiträge seit 2008
vor 3 Jahren

IIRC = if i remember correctly =)

L
lutzeslife Themenstarter:in
155 Beiträge seit 2012
vor 3 Jahren

😄 Achso ich versuche gerade die Stelle in den Docs zu finden.

Mit freundlichen Grüßen
lutzeslife

16.806 Beiträge seit 2008
vor 3 Jahren

Bin mir nicht sicher wo das steht, sonst hät ichs verlinkt 😉

6.911 Beiträge seit 2009
vor 3 Jahren

Hallo lutzeslife,

async void  

Sollte tunlichst vermieden werden, da es hier keinen Rückgabewert gibt und somit würde im nicht behandelten Fehlerfall der Prozess crashen.
Besser wäre ein async Task, das führt wenn die Exception nicht behandelt wird zu einer TaskScheduler.UnobservedTaskException, welche ihrerseits (global) behandelt werden kann.
Hier bei der Main-Methode ist es nicht so tragisch, aber bei anderen Methoden um so mehr.

Es gibt schon ein paar Unterschiede ob der Task direkt zurückgegeben wird od. per async / await innerhalb der Methode auf den anderen Task "gewartet"* wird.

Die Unterschiede zeigen sich v.a. wenn es um Exceptions geht. Mit async werden Exception vom synchronen Code-Teil (derjenige vor dem await) und die asynchronen (jene vom Task) zu asynchronen Exceptions normalisiert und im zurückgegeben Task bereitgestellt. Das erleichtert das Exception-Handling. Probiers mal mit dem Debugger aus und schau dir die Unterschiede an.

Ein weiterer Unterschied ist späteres ändern der Methode. Ein Fallstrick ist z.B. wenn ein using hinzugefügt wird. Z.B.


static Task<Person> GetPersonByKey(int key)
{
    using var db = new DbContext();
    return db.Persons.FindBykey(key);
}

Hier gibt es ein (potentielles) Problem. Welches schreib ich jetzt absichtlich nicht 😉
Korrekt sollte es


static async Task<Person> GetPersonByKey(int key)
{
    using var db = new DbContext();
    return await db.Persons.FindBykey(key);
}

sein, damit die Ressource erst dann freigegeben wird (Dispose) wenn die asynchrone Aktion erledigt ist und eben der Aktion nicht der Kontext unter den Füßen weggezogen wird.

Also wenn du auf Nummer sicher gehen willst -- und das solltest du -- so verwende async / await anstatt den Task direkt zurückzugegeben. Der Overhead von der async. Statusmaschine, welche der C# Compiler generiert fällt hier unter "premature optimization is the root of all evil" und für den Fall dass die Performance hier tatsächlich entscheidend ist gibt es fortgeschrittene Pattern / Möglichkeiten, die aber eher nur in bestimmten Libraries von Bedeutung sind.

  
        static async Task DoSomthingAsync3()  
        {  
            await Calling();  
            return Calling();  
        }  

Das ist wahrscheinlich nicht was du willst.
await Callling(); führt den Code nach dem await mit (mind.) 1ms Verzögerung aus (wegen dem Task.Delay(1)) und wenn dann die "Verzögerung" an den Aufrufer zurückgegeben wird, so wird dort wieder 1ms "gewartet"*.
Allgemeiner: die selbe Operation wird doppelt ausgeführt.

* gewartet wird nicht durch Blockieren, sondern der Code nach dem await wird erst dann ausgeführt wenn der Task fertig ist ~ asynchrones Warten

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!"

16.806 Beiträge seit 2008
vor 3 Jahren

Du hast recht, dass Unterschiede gibt, wenn es mehr als dem Return in der Methode gibt; die Frage bezog sich ja aber auf nur den Return.
Der Unterschied auf das Exception Handling erfolgt durch den technischen Overhead ("Task in Task"), was für den Consumer der Methode jedoch kein (realen) Unterschied darstellt.

Also wenn du auf Nummer sicher gehen willst -- und das solltest du -- so verwende async / await anstatt den Task direkt zurückzugegeben.

Das ist entgegen der "Empfehlung", dass - wenn es nur den Return gibt - async/await weggelassen werden soll.

Aber ja, der Fallstrick

static Task<Person> GetPersonByKey(int key)
{
    using var db = new DbContext();
    return db.Persons.FindBykey(key);
}

hat mich mal knapp 2 Wochen Suche gekostet 😉

2.078 Beiträge seit 2012
vor 3 Jahren

Da wäre doch ein Analyzer eine gute Idee, oder?

Also eine einfache Variante, die warnt, wenn eine Async-Methode nicht async ist, aber mehr als ein Return enthält.
Oder eine komplexere Variante, die erst warnt, wenn darin Dinge, wie z.B. usings enthalten sind.

Kennt jemand sowas?

Weil ich bin ehrlich:
An diesen Fallstrick hätte ich nicht gedacht, hatte aber zum Glück noch nie das Problem 😄

Ohne so einem Analyzer würde ich aber wahrscheinlich entgegen der Empfehlung handeln, einfach nur, um diesen Fallstrick zu vermeiden, besonders wenn es keinen Unterschied macht.
Sowas könnte man dann auch im Team in einer internen Guideline festlegen.

16.806 Beiträge seit 2008
vor 3 Jahren

Ohne so einem Analyzer würde ich aber wahrscheinlich entgegen der Empfehlung handeln, einfach nur, um diesen Fallstrick zu vermeiden, besonders wenn es keinen Unterschied macht.

Das ist auch vollkommen in Ordnung, wenn man unsicher ist.

6.911 Beiträge seit 2009
vor 3 Jahren

Hallo Abt,

Das ist entgegen der Empfehlung

Z.B. im ASP.Net Core-Repo ist die Empfehlung aber genau so dass await verwendet werden soll. Da spielen die o.g. Gründe eine gewisse Rolle.
BTW: gibt es eine Quelle für diese Empfehlung? Alleine wegen dem Fallstrick (ist mir auch passiert, daher kenn ich den 😉) ist es wohl besser auf Nummer sicher zu gehen.

Zumal ist

wenn es nur den Return gibt recht selten und die Gefahr groß dass die Methode (später einmal) doch mehr macht und so ein Race eingebaut wird ohne daran zu denken. Meine Empfehlung ist daher auf Nummer sicher zu gehen und await verwenden.

Exception Handling ... kein (realen) Unterschied darstellt.

Kommt darauf an was unter "realer Unterschied" verstanden wird.
Nehmen wir folgendes Beispiel:


using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static async Task Main(string[] args)
        {
            try
            {
                await RunAsync();
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex);
            }

            Console.WriteLine();

            try
            {
                await RunAsync1();
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex);
            }
        }

        private static Task RunAsync()
        {
            return DoAsync();
        }

        private static async Task RunAsync1()
        {
            await DoAsync();
        }

        private static Task DoAsync()
        {
            // Geek note: Könnte auch eine TaskCompletionSource<T> sein, aber das ist eine
            // Klasse und benötigt somit eine Allokation, welche durch das Struct vermieden 
            // werden kann ;-)
            var taskBuilder = new AsyncTaskMethodBuilder();
            taskBuilder.SetException(new Exception("Test"));
            return taskBuilder.Task;
        }
    }
}

Hier gibt es im Callstack sehr wohl einen realen Unterschied und das kann beim Lesen von Logs bzw. bei der Fehlersuche von großer Hilfe sein.

technischen Overhead ("Task in Task")

Wovon sprichst du hier?
Wenn du die vom C#-Compiler erzeugte Stackmaschine fürs async / await meinst, so hat das nichts mit "Task in Task" zu tun. Ganz grob wird da eine IAsyncStateMachine erzeugt, die intern einen AsyncTaskMethodBuilder (daher hab ich den auch oben im Beispiel zur Veranschaulichung verwendet) verwendet. Dieser "Builder" ist wie der Name schon sagt, der Erzeuger für den Task, welcher später einmal -- wenn der async Vorgang fertig ist -- das Ergebnis / Exception setzt. Es ist nur ein Task im Spiel.

Hallo Palladin007,

Da wäre doch ein Analyzer eine gute Idee, oder?

Hm, eigentlich ja. Einer für die aufgezählten Fälle ist sogar relativ einfach zu erstellen (einfach eine RegisterSyntaxNodeAction für using und dann die Syntax-Nodes durchwandern um zu schauen ob ein await vorhanden ist*). Cool wäre dann auch gleich ein Code-Fix dazu (der ist dann schon ein wenig aufwändiger).

Für async void gibt es einen: VSTHRD100 Avoid async void methods
Für den Fallstrick ist mir kein offizieller bekannt. In der dotnet-Organisation auf GitHub taucht das Problem wegen PR-Review nicht wirklich auf, daher hat wahrscheinlich noch keiner vom Team darüber nachgedacht. Du kannst aber gerne eine Issue dazu erstellen 😉

* wie so oft liegt der Teufel im Detail, da ja nicht nur using zählt, sondern auch try-finally-Äquivalente vom using die auch berücksichtigt werden müssten. Genauso wie ein return das vor dem using steht (stellt kein Race dar) und ev. noch etliche andere Fälle.

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!"

16.806 Beiträge seit 2008
vor 3 Jahren

BTW: gibt es eine Quelle für diese Empfehlung?

Nen Vortrag von Stephen Toub; wenn ich mich richtig erinner aus seiner Zen of Async Reihe.
Und neben vielen Treffen auf der vergeblichen Suche danach, die durchaus Deine Meinung bekräftigen findet sich da auch meine Ansicht 😃

eliding async and await in async methods

Zum Thema Analyzer:
RCS1174: Remove redundant async/await

6.911 Beiträge seit 2009
vor 3 Jahren

Hallo Abt,

Stephen Toub; wenn ich mich richtig erinner aus seiner Zen of Async Reihe.

Das war dann wohl so um 2010-11.

2018 hat Toub diese Empfehlung abgeben:

Zitat von: async/await
Recommend using async/await by default, and only removing them when doing optimizations because of known perf issues (or you’re implementing libraries you expect to be used on super hot paths). The one exception to this is for public methods that need to do argument validation, in which case just as with enumerables, you generally want to do that validation in non-async methods so that the ArgumentExceptions are propagated out of the method rather than being captured into the returned task

(und wohl dazugelernt, dass die 2011er Empfehlung doch zu viele Fallstricke hat).
Und mir ist die Ausnahme (Argumentvalidierung) wieder in den Sinn gekommen. Daher verwende ich oft folgendes Pattern (mit C# 8 statischen lokalen Funktionen):


public Task DoSomethingAsync(string? arg)
{
    if (arg == null) throw new ArgumentNullException(nameof(arg));

    return Core(arg);

    static async Task Core(string arg)
    {
        // ...
        await // ...
        // ...
    }
}

Zum Analyzer (an den Roslynator hatte ich bei meiner Suche gar nicht gedacht obwohl ich ihn verwende, danke!): Disable by default RCS1174: Remove redundant async/await 😉
=> RCS1229: Use async/await when necessary ist aber das von Palladin007 Angesprochene.

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!"

2.078 Beiträge seit 2012
vor 3 Jahren

=> RCS1229: Use async/await when necessary ist aber das von Palladin007 Angesprochene.

Genau, das meine ich, besten Dank 😃
Das scheint aber leider nur using-Blöcke zu unterstützen, nicht die using-Deklaration und auch keine anderen potentiellen Fallstricke, wie z.B. mit try-catch-finally.
Vielleicht kommt da noch mehr, aber using sollte erst Mal das Wichtigste sein.

Und mir ist die Ausnahme (Argumentvalidierung) wieder in den Sinn gekommen. Daher verwende ich oft folgendes Pattern (mit C# 8 statischen lokalen Funktionen)

Auch wieder so ein Punkt, auf den ich noch nicht geachtet habe, danke 😄

L
lutzeslife Themenstarter:in
155 Beiträge seit 2012
vor 3 Jahren

Erstmal ein Dankschön an alle die geantwortet haben. Die Variante mit Exceptionhandling sowie die Sache mit using, habe ich auch jetzt auf dem Schirm. Gerade das Kommentar von Toub finde ich dort sehr hilfreich.

Mit freundlichen Grüßen
lutzeslife