110

Is there any conceptual difference between the following two pieces of code:

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

and

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

Does the generated code differ, either?

EDIT: To avoid confusion with Task.Run, a similar case:

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

and

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

LATE UPDATE: In addition to the accepted answer, there is also a difference in how LocalCallContext gets handled: CallContext.LogicalGetData gets restored even where there is no asynchrony. Why?

Community
  • 1
  • 1
avo
  • 10,101
  • 13
  • 53
  • 81
  • 1
    Yes, it differs. And it differs a lot. otherwise there would be no point in using `await`/`async` at all :) – MarcinJuraszek Jan 09 '14 at 23:12
  • 1
    I think there are *two* questions here. **1.** Does the actual implementation of the method matter to its caller? **2.** Do the compiled representations of the two methods differ? – DavidRR May 26 '16 at 14:58

4 Answers4

103

One major difference is in exception propagation. An exception, thrown inside an async Task method, gets stored in the returned Task object and remains dormant until the task gets observed via await task, task.Wait(), task.Result or task.GetAwaiter().GetResult(). It is propagated this way even if thrown from the synchronous part of the async method.

Consider the following code, where OneTestAsync and AnotherTestAsync behave quite differently:

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

If I call DoTestAsync(OneTestAsync, -2), it produces the following output:

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

Note, I had to press Enter to see it.

Now, if I call DoTestAsync(AnotherTestAsync, -2), the code workflow inside DoTestAsync is quite different, and so is the output. This time, I wasn't asked to press Enter:

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

In both cases Task.Delay(-2) throws at the beginning, while validating its parameters. This might be a made-up scenario, but in theory Task.Delay(1000) may throw too, e.g., when the underlying system timer API fails.

On a side note, the error propagation logic is yet different for async void methods (as opposed to async Task methods). An exception raised inside an async void method will be immediately re-thrown on the the current thread's synchronization context (via SynchronizationContext.Post), if the current thread has one (SynchronizationContext.Current != null). Otherwise, it will be re-thrown via ThreadPool.QueueUserWorkItem). The caller doesn't have a chance to handle this exception on the same stack frame.

I posted some more details about TPL exception handling behaviour here and here.


Q: Is it possible to mimic the exception propagation behavior of async methods for non-async Task-based methods, so that the latter doesn't throw on the same stack frame?

A: If really needed, then yes, there is a trick for that:

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

Note however, under certain conditions (like when it's too deep on the stack), RunSynchronously could still execute asynchronously.


Another notable difference is that the async/await version is more prone to dead-locking on a non-default synchronization context. E.g., the following will dead-lock in a WinForms or WPF application:
static async Task TestAsync()
{
    await Task.Delay(1000);
}

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

Change it to a non-async version and it won't dead-lock:

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

The nature of the dead-lock is well explained by Stephen Cleary in his blog.

noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 3
    I believe the deadlock in the first example could be avoided by adding .ConfigureAwait(false) to the await line, as it only happens because the method is trying to return to the same execution context. So then the exeptions are the only difference that remains. – relatively_random Oct 11 '18 at 08:00
  • 2
    @relatively_random, your comment is correct, although the answer was about the difference between `return Task.Run()` and `await Task.Run(); return`, rather than `await Task.Run().ConfigureAwait(false); return` – noseratio Oct 20 '19 at 09:19
  • If you find the program closes after you hit Enter, make sure you do ctrl+F5 instead of F5. – David Klempfner Dec 11 '19 at 09:35
54

What is the difference between

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

and

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

?

I am confused by this question. Let me try to clarify by responding to your question with another question. What's the difference between?

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

and

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

?

Whatever the difference between my two things is, the same difference is between your two things.

svick
  • 236,525
  • 50
  • 385
  • 514
Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 24
    Of course! You've opened my eyes :) In the first case, I create a wrapper task, semantically close to `Task.Delay(1000).ContinueWith(() = {})`. In the second one, it's just `Task.Delay(1000)`. The difference is somewhat subtle, but significant. – avo Jan 10 '14 at 03:32
  • 4
    Could you explain a bit the difference ? actually i don't..Thank you – zheng yu Mar 26 '18 at 11:54
  • 6
    Given there is a subtle difference with sync contexts, and exception propagation I'd say the difference between async/await and function wrappers is not the same. – Cameron MacFarland Apr 23 '18 at 04:07
  • 1
    @CameronMacFarland: That's why I asked for clarification. The question asks is there a **conceptual difference** between the two. Well, I don't know. There certainly are many differences; do any of them count as "conceptual" differences? In my example with nested funcs there are also differences in error propagation; if the functions are closed over local state there are differences in local lifetimes, and so on. Are these "conceptual" differences? – Eric Lippert Apr 23 '18 at 04:12
  • 2
    When i look at your answer, actually it's a question in response to a question, I would conclude that both do the same, but the first `MakeFunction` is complicating things :-) – Legends Jun 22 '18 at 23:55
  • 16
    This is an old answer, but I believe given today, would have been downvoted. It does not answer the question, nor does it point the OP to a source from which he can learn. – Daniel Dror Sep 15 '19 at 07:51
  • 6
    @DanielDubovski: And yet, the original poster commented "Of course! You've opened my eyes. The difference is somewhat subtle, but significant". It sounds like the OP got a significant insight from the answer, which is why I gave it. You are of course free to downvote this answer which plainly helped the OP if you believe that it is not helpful, but what would be even better is if you would leave an answer of your own that models what you consider to be an ideal answer. That way we all learn from your wisdom. – Eric Lippert Dec 03 '20 at 02:01
12

The two examples do differ. When a method is marked with the async keyword, the compiler generates a state-machine behind the scenes. This is what is responsible for resuming continuations once an awaitable has been awaited.

In contrast, when a method is not marked with async you are losing the ability to await awaitables. (That is, within the method itself; the method can still be awaited by its caller.) However, by avoiding the async keyword, you are no longer generating the state-machine, which can add a fair bit of overhead (lifting locals to fields of the state-machine, additional objects to the GC).

In examples like this, if you are able to avoid async-await and return an awaitable directly, it should be done to improve the efficiency of the method.

See this question and this answer which are very similar to your question and this answer.

Community
  • 1
  • 1
Lukazoid
  • 19,016
  • 3
  • 62
  • 85
12
  1. The first method does not even compile.

    Since 'Program.TestAsync()' is an async method that returns 'Task', a return keyword must not be followed by an object expression. Did you intend to return 'Task<T>'?

    It has to be

    async Task TestAsync()
    {
        await Task.Run(() => DoSomeWork());
    }
    
  2. There is major conceptual difference between these two. The first one is asynchronous, the second one is not. Read Async Performance: Understanding the Costs of Async and Await to get a little more about internals of async/await.

  3. They do generate different code.

    .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
    

    and

    .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
  • 124,003
  • 15
  • 196
  • 263
  • @MarcinJuraszek, indeed it didn't compile. That was a typo, I am sure you got it right. Otherwise, a great answer, thanks! I thought C# might be smart enough to avoid generating a state machine class in the first case. – avo Jan 09 '14 at 23:30