Toute différence entre «wait Task.Run (); revenir;" et "retourner Task.Run ()"?

90

Existe-t-il une différence conceptuelle entre les deux morceaux de code suivants:

async Task TestAsync() 
{
    await Task.Run(() => DoSomeWork());
}

et

Task TestAsync() 
{
    return Task.Run(() => DoSomeWork());
}

Le code généré est-il différent non plus?

EDIT: Pour éviter toute confusion avec Task.Run, un cas similaire:

async Task TestAsync() 
{
    await Task.Delay(1000);
}

et

Task TestAsync() 
{
    return Task.Delay(1000);
}

MISE À JOUR TARDIVE: En plus de la réponse acceptée, il y a aussi une différence dans la façon dont LocalCallContextest gérée: CallContext.LogicalGetData est restauré même en l'absence d'asynchronie. Pourquoi?

éviter
la source
1
Oui, cela diffère. Et cela diffère beaucoup. sinon, il ne servirait à rien d'utiliser await/ asyncdu tout :)
MarcinJuraszek
1
Je pense qu'il y a deux questions ici. 1. La mise en œuvre réelle de la méthode est-elle importante pour son appelant? 2. Les représentations compilées des deux méthodes diffèrent-elles?
DavidRR

Réponses:

80

Une différence majeure réside dans la propagation des exceptions. Une exception, jetée à l' intérieur d' une async Taskméthode, est stockée dans le retour Taskobjet et reste en sommeil jusqu'à ce que la tâche s'observe via await task, task.Wait(), task.Resultou task.GetAwaiter().GetResult(). Il est propagé de cette manière même s'il est jeté de la partie synchrone de la asyncméthode.

Considérez le code suivant, où OneTestAsyncet AnotherTestAsyncse comportent assez différemment:

static async Task OneTestAsync(int n)
{
    await Task.Delay(n);
}

static Task AnotherTestAsync(int n)
{
    return Task.Delay(n);
}

// call DoTestAsync with either OneTestAsync or AnotherTestAsync as whatTest
static void DoTestAsync(Func<int, Task> whatTest, int n)
{
    Task task = null;
    try
    {
        // start the task
        task = whatTest(n);

        // do some other stuff, 
        // while the task is pending
        Console.Write("Press enter to continue");
        Console.ReadLine();
        task.Wait();
    }
    catch (Exception ex)
    {
        Console.Write("Error: " + ex.Message);
    }
}

Si j'appelle DoTestAsync(OneTestAsync, -2), il produit la sortie suivante:

Appuyez sur Entrée pour continuer
Erreur: une ou plusieurs erreurs se sont produites.
Erreur: 2ème

Remarque, j'ai dû appuyer Enterpour le voir.

Maintenant, si j'appelle DoTestAsync(AnotherTestAsync, -2), le flux de travail du code à l'intérieur DoTestAsyncest assez différent, tout comme la sortie. Cette fois, on ne m'a pas demandé d'appuyer sur Enter:

Erreur: la valeur doit être -1 (signifiant un délai d'attente infini), 0 ou un entier positif.
Nom du paramètre: millisecondesDelayError: 1st

Dans les deux cas Task.Delay(-2)jette au début, tout en validant ses paramètres. Cela peut être un scénario inventé, mais en théorie, cela Task.Delay(1000)peut aussi être déclenché, par exemple, lorsque l'API du minuteur système sous-jacent échoue.

Par ailleurs, la logique de propagation des erreurs est encore différente pour les async voidméthodes (par opposition aux async Taskméthodes). Une exception levée à l'intérieur d'une async voidméthode sera immédiatement relancée sur le contexte de synchronisation du thread courant (via SynchronizationContext.Post), si le thread actuel en a un ( SynchronizationContext.Current != null). Sinon, elle sera relancée via ThreadPool.QueueUserWorkItem). L'appelant n'a pas la possibilité de gérer cette exception sur le même cadre de pile.

J'ai publié quelques détails supplémentaires sur le comportement de gestion des exceptions TPL ici et ici .


Q : Est-il possible d'imiter le comportement de propagation des exceptions des asyncméthodes pour les méthodes non asynchrones Task, de sorte que ces dernières ne lancent pas sur le même cadre de pile?

R : Si vraiment nécessaire, alors oui, il y a une astuce pour cela:

// async
async Task<int> MethodAsync(int arg)
{
    if (arg < 0)
        throw new ArgumentException("arg");
    // ...
    return 42 + arg;
}

// non-async
Task<int> MethodAsync(int arg)
{
    var task = new Task<int>(() => 
    {
        if (arg < 0)
            throw new ArgumentException("arg");
        // ...
        return 42 + arg;
    });

    task.RunSynchronously(TaskScheduler.Default);
    return task;
}

Notez cependant que dans certaines conditions (comme lorsqu'il est trop profond sur la pile), il RunSynchronouslypeut toujours s'exécuter de manière asynchrone.


Une autre différence notable est que la version async/ awaitest plus sujette au blocage dans un contexte de synchronisation autre que celui par défaut . Par exemple, ce qui suit sera verrouillé dans une application WinForms ou WPF:

static async Task TestAsync()
{
    await Task.Delay(1000);
}

void Form_Load(object sender, EventArgs e)
{
    TestAsync().Wait(); // dead-lock here
}

Changez-le en une version non asynchrone et il ne sera pas verrouillé:

Task TestAsync() 
{
    return Task.Delay(1000);
}

La nature de l'impasse est bien expliquée par Stephen Cleary dans son blog .

noseratio
la source
2
Je pense que le blocage dans le premier exemple pourrait être évité en ajoutant .ConfigureAwait (false) à la ligne d'attente, car cela se produit uniquement parce que la méthode tente de revenir au même contexte d'exécution. Les exonérations sont donc la seule différence qui reste.
relativement_random
2
@relativement_random, votre commentaire est correct, même si la réponse concernait la différence entre return Task.Run()et await Task.Run(); return, plutôt queawait Task.Run().ConfigureAwait(false); return
noseratio
Si vous trouvez que le programme se ferme après avoir appuyé sur Entrée, assurez-vous de faire ctrl + F5 au lieu de F5.
David Klempfner
54

Quelle est la différence entre

async Task TestAsync() 
{
    await Task.Delay(1000);
}

et

Task TestAsync() 
{
    return Task.Delay(1000);
}

?

Je suis confus par cette question. Permettez-moi d'essayer de clarifier en répondant à votre question par une autre question. Quelle est la différence entre?

Func<int> MakeFunction()
{
    Func<int> f = ()=>1;
    return ()=>f();
}

et

Func<int> MakeFunction()
{
    return ()=>1;
}

?

Quelle que soit la différence entre mes deux choses, la même différence existe entre vos deux choses.

Eric Lippert
la source
22
Bien sûr! Vous avez ouvert les yeux :) Dans le premier cas, je crée une tâche wrapper, sémantiquement proche de Task.Delay(1000).ContinueWith(() = {}). Dans le second, c'est juste Task.Delay(1000). La différence est quelque peu subtile, mais significative.
éviter le
3
Pouvez-vous expliquer un peu la différence? en fait je ne ... Merci
zheng yu
4
Étant donné qu'il existe une différence subtile avec les contextes de synchronisation et la propagation d'exceptions, je dirais que la différence entre async / await et les wrappers de fonction n'est pas la même.
Cameron MacFarland
1
@CameronMacFarland: C'est pourquoi j'ai demandé des éclaircissements. La question se pose est-il une différence conceptuelle entre les deux. Eh bien, je ne sais pas. Il existe certainement de nombreuses différences; est-ce que l'un d'entre eux compte comme des différences «conceptuelles»? Dans mon exemple avec des fonctions imbriquées, il existe également des différences dans la propagation des erreurs; si les fonctions sont fermées sur l'état local, il existe des différences de durée de vie locale, et ainsi de suite. S'agit-il de différences «conceptuelles»?
Eric Lippert
6
C'est une vieille réponse, mais je crois que donnée aujourd'hui, aurait été rejetée. Il ne répond pas à la question, ni ne pointe le PO vers une source à partir de laquelle il peut apprendre.
Daniel Dubovski
11
  1. La première méthode ne compile même pas.

    Puisque « Program.TestAsync()» est une méthode asynchrone qui renvoie « Task», un mot-clé de retour ne doit pas être suivi d'une expression d'objet. Aviez-vous l'intention de revenir ' Task<T>'?

    Ça doit être

    async Task TestAsync()
    {
        await Task.Run(() => DoSomeWork());
    }
    
  2. Il existe une différence conceptuelle majeure entre les deux. Le premier est asynchrone, le second ne l'est pas. Lisez Async Performance: Understanding the Costs of Async and Await pour en savoir plus sur les composants internes de async/ await.

  3. Ils génèrent un code différent.

    .method private hidebysig 
        instance class [mscorlib]System.Threading.Tasks.Task TestAsync () cil managed 
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = (
            01 00 25 53 4f 54 65 73 74 50 72 6f 6a 65 63 74
            2e 50 72 6f 67 72 61 6d 2b 3c 54 65 73 74 41 73
            79 6e 63 3e 64 5f 5f 31 00 00
        )
        .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x216c
        // Code size 62 (0x3e)
        .maxstack 2
        .locals init (
            [0] valuetype SOTestProject.Program/'<TestAsync>d__1',
            [1] class [mscorlib]System.Threading.Tasks.Task,
            [2] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder
        )
    
        IL_0000: ldloca.s 0
        IL_0002: ldarg.0
        IL_0003: stfld class SOTestProject.Program SOTestProject.Program/'<TestAsync>d__1'::'<>4__this'
        IL_0008: ldloca.s 0
        IL_000a: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create()
        IL_000f: stfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder'
        IL_0014: ldloca.s 0
        IL_0016: ldc.i4.m1
        IL_0017: stfld int32 SOTestProject.Program/'<TestAsync>d__1'::'<>1__state'
        IL_001c: ldloca.s 0
        IL_001e: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder'
        IL_0023: stloc.2
        IL_0024: ldloca.s 2
        IL_0026: ldloca.s 0
        IL_0028: call instance void [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start<valuetype SOTestProject.Program/'<TestAsync>d__1'>(!!0&)
        IL_002d: ldloca.s 0
        IL_002f: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder'
        IL_0034: call instance class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task()
        IL_0039: stloc.1
        IL_003a: br.s IL_003c
    
        IL_003c: ldloc.1
        IL_003d: ret
    } // end of method Program::TestAsync
    

    et

    .method private hidebysig 
        instance class [mscorlib]System.Threading.Tasks.Task TestAsync2 () cil managed 
    {
        // Method begins at RVA 0x21d8
        // Code size 23 (0x17)
        .maxstack 2
        .locals init (
            [0] class [mscorlib]System.Threading.Tasks.Task CS$1$0000
        )
    
        IL_0000: nop
        IL_0001: ldarg.0
        IL_0002: ldftn instance class [mscorlib]System.Threading.Tasks.Task SOTestProject.Program::'<TestAsync2>b__4'()
        IL_0008: newobj instance void class [mscorlib]System.Func`1<class [mscorlib]System.Threading.Tasks.Task>::.ctor(object, native int)
        IL_000d: call class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Threading.Tasks.Task::Run(class [mscorlib]System.Func`1<class [mscorlib]System.Threading.Tasks.Task>)
        IL_0012: stloc.0
        IL_0013: br.s IL_0015
    
        IL_0015: ldloc.0
        IL_0016: ret
    } // end of method Program::TestAsync2
    
MarcinJuraszek
la source
@MarcinJuraszek, en effet, il n'a pas été compilé. C'était une faute de frappe, je suis sûr que vous avez bien compris. Sinon, une excellente réponse, merci! Je pensais que C # pourrait être assez intelligent pour éviter de générer une classe de machine à états dans le premier cas.
éviter le
9

Les deux exemples ne diffèrent. Lorsqu'une méthode est marquée avec le asyncmot - clé, le compilateur génère une machine d'état dans les coulisses. C'est ce qui est responsable de la reprise des continuations une fois qu'un attendu a été attendu.

En revanche, lorsqu'une méthode n'est pas marquée avec, asyncvous perdez la capacité des awaitWaitables. (C'est-à-dire dans la méthode elle-même; la méthode peut toujours être attendue par son appelant.) Cependant, en évitant le asyncmot - clé, vous ne générez plus la machine à états, ce qui peut ajouter un peu de surcharge (lever les locaux vers les champs de la machine à états, des objets supplémentaires au GC).

Dans des exemples comme celui-ci, si vous êtes en mesure d'éviter async-awaitet de renvoyer directement un fichier attendu, cela doit être fait pour améliorer l'efficacité de la méthode.

Voir cette question et cette réponse qui sont très similaires à votre question et à cette réponse.

Lukazoïde
la source