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

Любая разница между "ждут Task.Run(); return;" и "return Task.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 восстанавливается даже там, где нет асинхронности. Почему?

4b9b3361

Ответ 1

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

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


Другое существенное отличие заключается в распространении исключений. Исключение, созданное внутри метода 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 все равно может выполняться асинхронно.

Ответ 2

В чем разница между

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

?

Какая разница между двумя моими вещами, та же разница между вашими двумя вещами.

Ответ 3

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

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

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

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

  • Они генерируют другой код.

    .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
    

Ответ 4

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

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

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

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