Любая разница между «await Task.Run (); возвращение;" и «вернуть Task.Run ()»?

90

Есть ли концептуальная разница между следующими двумя частями кода:

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

а также

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

Сгенерированный код тоже отличается?

РЕДАКТИРОВАТЬ: Чтобы избежать путаницы с Task.Runаналогичным случаем:

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

а также

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

ПОСЛЕДНЕЕ ОБНОВЛЕНИЕ: помимо принятого ответа, также есть разница в том, как LocalCallContextобрабатывается: CallContext.LogicalGetData восстанавливается даже там, где нет асинхронности. Зачем?

избегать
источник
1
Да, отличается. И он сильно отличается. иначе не было бы смысла использовать await/ asyncвообще :)
MarcinJuraszek 09
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/ awaitверсия более подвержена мертвой блокировке в контексте синхронизации, отличном от используемого по умолчанию . Например, в приложении 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) в строку ожидания, поскольку это происходит только потому, что метод пытается вернуться в тот же контекст выполнения. Итак, исключения - единственное, что остается.
relative_random
2
@relatively_random, ваш комментарий верен, хотя ответ был о разнице между return Task.Run()и await Task.Run(); return, а неawait Task.Run().ConfigureAwait(false); return
noratio
Если вы обнаружите, что программа закрывается после нажатия 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). Разница несколько тонкая, но существенная.
избежать
3
Не могли бы вы немного объяснить разницу? на самом деле я не ... Спасибо
zheng yu
4
Учитывая небольшую разницу в контекстах синхронизации и распространении исключений, я бы сказал, что разница между оболочками async / await и функциями не одинакова.
Кэмерон МакФарланд
1
@CameronMacFarland: Вот почему я попросил разъяснений. Вопрос спрашивает, есть ли между ними концептуальная разница . Ну не знаю. Конечно, есть много различий; Считается ли какое-либо из них «концептуальным» различием? В моем примере с вложенными функциями также есть различия в распространении ошибок; если функции закрыты по локальному состоянию, существуют различия в локальных временах жизни и так далее. Это «концептуальные» различия?
Эрик Липперт
6
Это старый ответ, но я считаю, что данный сегодня он был бы отвергнут. Он не отвечает на вопрос и не указывает OP на источник, из которого он может учиться.
Даниэль Дубовски
11
  1. Первый метод даже не компилируется.

    Поскольку " Program.TestAsync()" является асинхронным методом, возвращающим " Task", за ключевым словом return не должно следовать выражение объекта. Ты собирался вернуться Task<T>? ' '

    Должно быть

    async Task TestAsync()
    {
        await Task.Run(() => DoSomeWork());
    }
    
  2. Между этими двумя понятиями существует большая концептуальная разница. Первый асинхронный, второй - нет. Прочтите Async Performance: Understanding the Costs of Async and 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 # может быть достаточно умен, чтобы избежать создания класса конечного автомата в первом случае.
избежать
9

Эти два примера действительно различаются. Когда метод помечен asyncключевым словом, компилятор генерирует конечный автомат за кулисами. Это то, что отвечает за возобновление продолжения после ожидания ожидаемого.

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

В таких примерах, как этот, если вы можете избежать async-awaitи напрямую вернуть ожидаемый объект, это должно быть сделано для повышения эффективности метода.

См. Этот вопрос и этот ответ, которые очень похожи на ваш вопрос и этот ответ.

Луказоид
источник