Будь-яка різниця між “await Task.Run (); повернення; " і “повернути Task.Run ()”?


90

Чи існує концептуальна різниця між двома наступними фрагментами коду:

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

і

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

Чи відрізняється згенерований код?

EDIT: Щоб уникнути плутанини Task.Run, подібний випадок:

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

і

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

ПІЗНЕ ОНОВЛЕННЯ: На додаток до прийнятої відповіді, є також різниця в способіLocalCallContext обробки: CallContext.LogicGetData відновлюється навіть там, де немає асинхронності. Чому?


1
Так, це відрізняється. І це сильно відрізняється. інакше не було б сенсу використовувати await/ asyncвзагалі :)
MarcinJuraszek

1
Думаю, тут є два запитання. 1. Чи реальна реалізація методу має значення для його абонента? 2. Чи відрізняються складені подання двох методів?
DavidRR

Відповіді:


80

Одна велика відмінність полягає у поширенні винятків. Виняток, кинуте всередині async Taskметоди, зберігається в повернутому Taskоб'єкті і залишається бездіяльним , поки завдання не отримує спостерігаються через await task, task.Wait(), task.Resultабо task.GetAwaiter().GetResult(). Він поширюється таким чином, навіть якщо викинутий із синхронної частини asyncметоду.

Розглянемо наступний код, де OneTestAsyncі AnotherTestAsyncповодимось зовсім інакше:

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);
    }
}

Якщо я зателефоную DoTestAsync(OneTestAsync, -2), це видасть такий результат:

Натисніть Enter, щоб продовжити
Помилка: сталася одна або декілька помилок. Чекайте Task.Delay
Помилка: 2-а

Зауважте, мені довелося натиснути, Enterщоб побачити це.

Тепер, якщо я зателефоную DoTestAsync(AnotherTestAsync, -2), робочий процес коду всередині DoTestAsyncзовсім інший, як і результат. Цього разу мене не просили натиснути Enter:

Помилка: значення має бути -1 (що означає нескінченний тайм-аут), 0 або ціле додатне число.
Назва параметра: millisecondsDelayError: 1st

В обох випадках Task.Delay(-2)кидки на початку, одночасно перевіряючи його параметри. Це може бути вигаданим сценарієм, але теоретично Task.Delay(1000)це теж може виникнути, наприклад, коли базовий API системного таймера виходить з ладу.

До речі, логіка поширення помилок все ж відрізняється для async voidметодів (на відміну від async Taskметодів). Виняток, викликаний усередині async voidметоду, буде негайно перекинуто в контекст синхронізації поточного потоку (через SynchronizationContext.Post), якщо поточний потік має такий ( SynchronizationContext.Current != null). В іншому випадку він буде повторно переданий через ThreadPool.QueueUserWorkItem). Абонент не має можливості обробити цей виняток на тому самому фреймі стека.

Деякі докладніші відомості про поведінку винятків TPL я розмістив тут і тут .


З : Чи можна імітувати поведінку asyncметодів розповсюдження винятків для методів, що не базуються Taskна асинхронізації , щоб останні не кидали на той самий кадр стека?

В : Якщо насправді потрібно, то так, для цього є хитрість:

// 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;
}

Однак зауважте, що за певних умов (наприклад, коли це занадто глибоко в стеці) RunSynchronouslyвсе одно може виконуватися асинхронно.


Інша помітна відмінність полягає в тому, що async/ awaitversion є більш схильним до блокування в контексті синхронізації, який не за замовчуванням . Наприклад, у програмі WinForms або WPF буде заблоковано наступне:

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

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

Змініть його на несинхронну версію, і вона не заблокує:

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

Природа глухого замку добре пояснює Стівен Клірі у своєму блозі .


2
Я вважаю, що блокування в першому прикладі можна уникнути, додавши .ConfigureAwait (false) до рядка await, оскільки це відбувається лише тому, що метод намагається повернутися до того самого контексту виконання. Тоді винятки - це єдина різниця, яка залишається.
relatively_random

2
@relatively_random, ваш коментар правильний, хоча відповідь return Task.Run()await Task.Run(); returnawait Task.Run().ConfigureAwait(false); return
стосувалася

Якщо ви виявите, що програма закривається після натискання клавіші Enter, обов’язково використовуйте ctrl + F5 замість F5.
Девід Клемпфнер,

54

Яка різниця між

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

і

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

?

Мене бентежить це питання. Дозвольте мені спробувати пояснити, відповівши на ваше запитання іншим запитанням. Яка різниця між ними?

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

і

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

?

Якою б не була різниця між моїми двома речами, однакова різниця між вашими двома речами.


23
Звичайно! Ви відкрили мені очі :) У першому випадку я створюю завдання-обгортку, семантично близьку до Task.Delay(1000).ContinueWith(() = {}). У другому - це просто Task.Delay(1000). Різниця дещо незначна, але значна.
avo

3
Не могли б ви трохи пояснити різницю? насправді я не .. Дякую
zheng yu

4
Враховуючи незначну різницю в контексті синхронізації, і розповсюдження винятків, я б сказав, що різниця між обгортками async / await та функції не однакова.
Cameron MacFarland

1
@CameronMacFarland: Ось чому я попросив пояснити. Задає питання, чи існує між ними концептуальна різниця . Ну, я не знаю. Існує безліч відмінностей; чи вважається хтось із них "концептуальними" відмінностями? У моєму прикладі з вкладеними функціями також є відмінності у поширенні помилок; якщо функції закриті над місцевою державою, існують відмінності у місцевому житті тощо. Це "концептуальні" відмінності?
Ерік Ліпперт,

6
Це стара відповідь, але, на мою думку, сьогоднішню відповідь було б проти. Він не відповідає на запитання і не вказує ОП на джерело, з якого він може вчитися.
Даніель Дубовський

11
  1. Перший метод навіть не компілюється.

    Оскільки ' Program.TestAsync()' є асинхронним методом, який повертає ' Task', ключове слово return не повинно супроводжуватися виразом об'єкта. Ви мали намір повернутися ' Task<T>'?

    Це повинно бути

    async Task TestAsync()
    {
        await Task.Run(() => DoSomeWork());
    }
    
  2. Між цими двома існує велика концептуальна різниця. Перший - асинхронний, другий - ні. Прочитайте Async Performance: Розуміння витрат на Async та Await, щоб отримати трохи більше інформації про внутрішні компоненти async/ await.

  3. Вони генерують різний код.

    .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
    

    і

    .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, справді він не компілювався. Це була друкарська помилка, я впевнений, ви правильно зрозуміли. В іншому випадку, чудова відповідь, дякую! Я думав, що C # може бути достатньо розумним, щоб уникнути генерування класу автоматів у першому випадку.
avo

9

Ці два приклади дійсно відрізняється. Коли метод позначений asyncключовим словом, компілятор генерує автомат стану за лаштунками. Це те, що відповідає за відновлення продовження, коли очікувалося очікуване.

На відміну від цього, коли метод не позначений, asyncви втрачаєте здатність до awaitочікуваного. (Тобто всередині самого методу; метод все ще може бути очікуваний його абонентом.) Однак, уникаючи asyncключового слова, ви більше не генеруєте автомат стану, який може додати неабиякі накладні витрати (піднімаючи місцевих жителів на поля автомата, додаткові об'єкти до GC).

У таких прикладах, якщо ви можете уникнути async-awaitі повернути очікуване безпосередньо, це слід зробити для підвищення ефективності методу.

Перегляньте це запитання та цю відповідь, які дуже схожі на ваше запитання та цю відповідь.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.