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

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

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.LogicalGetData восстанавливается даже при отсутствии асинхронности. Почему?

4 ответов


Обновлено, помимо различий в поведении распространения исключений, описанных ниже, есть еще одно несколько тонкое различие: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);
}

природа мертвого замка хорошо объяснена Стивен Клири в своем блог.


Еще одно существенное различие заключается в распространение исключения. исключение, брошенное внутри 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), он производит следующий вывод:

Press enter to continue
Error: One or more errors occurred.await Task.Delay
Error: 2nd

Примечание, я должен был нажать Enter чтобы увидеть его.

теперь, если я называю DoTestAsync(AnotherTestAsync, -2) рабочий процесс внутри DoTestAsync совсем другое, и поэтому выход. На этот раз меня не просили нажимать Enter:

Error: The value needs to be either -1 (signifying an infinite timeout), 0 or a positive integer.
Parameter name: millisecondsDelayError: 1st

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

на боковом примечании логика распространения ошибок еще отличается для async void методы (вместо async Task методов). Исключение, вызванное внутри async void метод будет немедленно повторно брошен в контекст синхронизации текущего потока (через SynchronizationContext.Post), если текущий поток один (SynchronizationContext.Current != null). В противном случае, он будет повторно брошен через ThreadPool.QueueUserWorkItem). У вызывающего абонента нет возможности обработать это исключение в том же кадре стека.

я опубликовал дополнительные сведения о поведении обработки исключений TPL здесь и здесь.


Q: можно ли имитировать поведение распространения исключений async методы неасинхронным Task-основанные методы, так что последний не бросает на тот же кадр стека?

A: если действительно нужно, то да, есть трюк для этого:

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

?

что разница между двумя вещами есть разница между двумя вещами.


  1. первый метод даже не компилируется.

    СProgram.TestAsync() - это асинхронный метод, который возвращает 'Task', за ключевым словом return не должно следовать выражение объекта. Вы намеревались вернуться?--5-->'?

    это должно быть

    async Task TestAsync()
    {
        await Task.Run(() => DoSomeWork());
    }
    
  2. существует большая концептуальная разница между этими двумя. Первый-асинхронный, второй-нет. Чтение Асинхронной Производительности: Понимание стоимости 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 CS00
        )
    
        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
    

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

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

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

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