Любая разница между " ожидание задачи.Выполнить(); возврат;" и "обратная задача.Выполнить()"?


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

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 68

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

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


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

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

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

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 не должно сопровождаться выражением объекта. Вы собирались вернуться?Task<T>'?

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

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

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

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