2

This is a linqpad example of two ways of executing a method asynchronously after a short delay. Both examples appear to do exactly the same thing. I would normally implement the first version (using Task.Delay.ContinueWith), but I've seen the second implementation used (async await) too. Is there any difference at all between these two implementations? Working Linqpad example of this scenario:

void Main()
{
   // Using Task.Delay.ContinueWith...
   Task.Delay(1000).ContinueWith(t => DoSomething());       

   // ... vs async await. Note that I'm not awaiting the task here
   DoSomethingAsync();
}

public void DoSomething()
{
    "Doing Something...".Dump();
}

public async Task DoSomethingAsync()
{
    await Task.Delay(1000);

    "Doing Something...".Dump();
}

After reading this blog post https://blogs.msdn.microsoft.com/pfxteam/2012/03/24/should-i-expose-asynchronous-wrappers-for-synchronous-methods/ I assumed the first implementation was the 'correct' one because the 'DoSomethingAsync()' is really only offloading the method to the threadpool and the blog post states:

"Asynchronous methods should not be exposed purely for the purpose of offloading: such benefits can easily be achieved by the consumer of synchronous methods using functionality specifically geared towards working with synchronous methods asynchronously, e.g. Task.Run."

However, this answer on StackOverflow suggests the second solution:

Delay then execute Task

Is there any actual difference between the two implementations? If the 'async await' implementation is also valid (or more correct even), what should be done with the returned Task? I don't really want to wait for it, this is a fire-and-forget operation, but I also want to handle any exceptions that might be thrown.

In the first implementation I know I could handle the exception by using a ContinueWith, OnlyOnFaulted.

JMc
  • 971
  • 2
  • 17
  • 26
  • 1
    Where do you think your continuation is running under option 1 if not on the thread pool? – Damien_The_Unbeliever May 22 '18 at 08:36
  • 1
    The ContinueWith delegate will run on the same thread as the original task, as its just a continuation of it. If you use await it will use another thread pool thread as its a different task. You can check this by outputting Thread.CurrentThread.ManagedThreadId. – ckuri May 22 '18 at 08:38
  • Just as `DoSomethingAsync` returns a task so does `ContinueWith`. – Magnus May 22 '18 at 08:46
  • @Damien_The_Unbeliever - I understand the ContinueWith will run on a ThreadPool thread. That's what I want really. – JMc May 22 '18 at 08:54

2 Answers2

4

They are similar, but not exactly the same. For example, in precence of SynchronizationContext.Current, continuation of async method will be scheduled to this synchronization context, but ContinueWith will not, and will run on thread pool thread. Though using another overload of ContinueWith you can make it do the same:

.ContinueWith(t => DoSomething(), TaskScheduler.FromCurrentSynchronizationContext());

And you can prevent scheduling to synchronization context in async version with await yourTask.ConfigureAwait(false).

Then, exception handling is different. In async version, exception will be thrown directly, and if there were multiple exceptions (such as from await Task.WhenAll) - only first will be thrown and the rest will be swallowed. It's hard to miss this exception.

In ContinueWith version, thrown exception is represented by t.Exception, and it's always an AggregateException, so you have to unwrap it. On the other hand - all exceptions are there (in case there are multiple) and none are swallowed. However, it's quite easy to forget to handle that exception. In code in your question for example - you don't handle exception in ContinueWith and so DoSomething() continuation will be executed anyway, whether there were an exception or not. In async version continuation is not executed in case of exception.

Both implementations are "valid". You should just not forget to handle exceptions in both cases. With ContinueWith - either always check t.Exception, or schedule separate continuation with OnlyOnFaulted (and check only there). In case of async version - wrap body in try-catch block. By the nature of fire and forget - you cannot handle exceptions at the call site, but you should not completely abandon them.

In your specific case with executing method after short delay, I'd say it's just a matter of preference (except for differences in capturing sync context). I personally prefer ContinueWith, it expresses intent more clear and you don't need separate method with unclear semantics.

Evk
  • 98,527
  • 8
  • 141
  • 191
  • Could both of the suggested implementations result in a crashed process, as described in https://blogs.msdn.microsoft.com/pfxteam/2009/06/01/tasks-and-unhandled-exceptions/ then? – JMc May 22 '18 at 09:04
  • @JMc async version cannot result in that. `ContinueWith` version can, if you don't observe exception as suggested (by accessing `Exception` property), and if you are running on .NET version with behavior described in your link. Note that starting from .NET 4.5 - unobserved task exceptions do NOT crash the process. – Evk May 22 '18 at 09:09
1

ContinueWith() allows you to chain tasks in "fire and forget" style. Otherwise, you have to await for the task completion and then do next awaitable operation (and "await" means you have to modify the caller method signature with "async" as well, that could be not so suitable in, let say, event handlers).

Yury Schkatula
  • 5,291
  • 2
  • 18
  • 42
  • 1
    Actually, ContinueWith() does wait for the target Task to finish and then continue. Please check- https://msdn.microsoft.com/en-us/library/dd270696(v=vs.110).aspx – Souvik Ghosh May 22 '18 at 08:40
  • Technically you don't modify the method signature by using async. Async is just a marker that the await keyword is allowed. The compiled code is the same for a method with and without the async keyword. – ckuri May 22 '18 at 08:40
  • @SouvikGhosh, absolutely, it chains the tasks and then execute them one-by-one, waiting each to complete before calling next one. – Yury Schkatula May 22 '18 at 08:44
  • @ckuri, well, yes and no. Having just "async" added is non-changing thing itself. Same time, it's dangerous to do so on void-returning methods (like most of event handlers) because you loose a stacktrace in case of exception and it is not being caught properly then. – Yury Schkatula May 22 '18 at 08:47