41

Consider this example:

var task = DoSomething()
bool ready = await DoSomethingElse();
if (!ready) 
  return null;

var value = await DoThirdThing(); // depends on DoSomethingElse
return value + await task;

DoSomething does some very important work that may take a while, thus we start it off first.
In the meantime we check whether we're ready with DoSomethingElse and exit early if not.
We call DoThirdThing only if we are ready, as the universe might otherwise explode.

We cannot use Task.WhenAll as DoThirdThing depends on DoSomethingElse and we also don't want to wait for DoSomething because we want to call the other two methods concurrently if possible.

The question: What happens to task if we're not ready and exit early?

Will any exceptions that it throws be re-thrown by a SynchronizationContext?
Are there problems if task completes normally, as nobody consumes its value?

Follow-up: Is there a neat way to make sure task is awaited?

We could simply await task if we are not ready, however if there were 50 exit conditions this would be very tedious.
Could a finally block be used to await task and re-throw potential exceptions? If task completed normally it would be awaited again in the finally block, but that shouldn't cause any problems?

enzi
  • 4,057
  • 3
  • 35
  • 53
  • 3
    You can add a continuation on a task that only executes if the task faults using a`Task.ContinueWith` overload that takes a `TaskContinuationOptions` as a parameter, specifying `TaskContinuationOptions.OnlyOnFaulted` – Preston Guillot Oct 12 '15 at 17:12
  • I was considering to do that, but thought there might be other ways to achieve the same, seeing as there are many SO questions were people advice against `ContinueWith` and in favor of `await` due to simpler control flow. Definitely a clean and concise option, though. – enzi Oct 12 '15 at 19:31
  • 1
    @PrestonGuillot: There is absolutely no reason to use `ContinueWith` here. It is merely an extremely dangerous version of `await`, and should not be used. – Stephen Cleary Oct 12 '15 at 22:13
  • @enzi, I posted a follow-up question [here](http://stackoverflow.com/q/33446488/2674222). – avo Oct 30 '15 at 23:57
  • Parallel execution is not achieved by just using `async/await`. You could use `Task.Run` to start the long-running process on a worker thread. Alternatively, you could consider `WhenAny` or the `Parallel` class. For your case, one approach could be: 1. `await DoSomethingElse()`. 2. `await Task.WhenAll(DoThirdThing(), DoSomething())`. This way, you can first take into account the result of `DoSomethingElse`, after which the other two tasks can run in parallel. – Timo May 04 '18 at 12:38
  • @Timo the point of the exercise is to start `DoSomething` ASAP since it takes a while to complete, deferring it defeats the purpose. I also don't see how`Task.Run` or `Parallel` are relevant to this discussion. If `DoSomething` is a long-running asynchronous operation, why would I waste a worker thread on it? And again, it sidesteps the actual question, which is what happens to not-awaited async tasks. Stephen Clearly's answer explains this very well. – enzi May 04 '18 at 16:08
  • @envi The question's words "in the meantime we check" and "call the other two methods concurrently" indicate that parallel execution is desirable. However, this is not achieved by `async/await`. `async/await` merely keeps us from blocking the current thread while waiting for some I/O to complete (i.e. freeing up a CPU core while we wait for the disk, network, etc.). This is why I suggested `Task.Run`, `Task.WhenAny`, or `Parallel`. Such methods will let us run the other two methods concurrently, or do other things while the long-running task is running. – Timo May 11 '18 at 11:10
  • @Timo parallel execution _is_ possible with `async/await` constructs. If an async operation awaits the completion of some I/O operation, as you describe, it will return control to the caller. The caller can then execute arbitrary code _in parallel_ until he explicitly `await`s the incomplete Task. The incomplete Task will independently resume execution when its I/O operation was completed, whether or not the caller `await`ed it. Parallel execution won't happen if there's no I/O operation or similar in the async method or a `SynchronizationContext` forces all execution to run on a single thread – enzi May 11 '18 at 12:27
  • @enzi Ah, good point. I've been wondering, is the task execution guaranteed to start, in the absence of an `await` by the caller? Also, is this approach considered good/common practice? – Timo May 11 '18 at 14:34
  • The async method is executed sequentially until it itself first `awaits` something, so yes it is always executed. If no `await` is hit (e.g. if an already computed values is returned from a cache) the whole method will be executed sequentially. Interweaving code (not immediately awaiting) is common, not awaiting at all is not common, not awaiting `async void` is a discouraged anti-pattern. I can only recommend you check out the work of Stephen Clearly (accepted answer here), especially his [Concurrency in C# Cookbook](https://stephencleary.com/book/) for details & best practices – enzi May 11 '18 at 15:37

3 Answers3

25

The question: What happens to task if we're not ready and exit early?

Nothing. The code ignores the task, so the task is ignored.

Will any exceptions that it throws be re-thrown by a SynchronizationContext?

No. They will (eventually) be passed to TaskScheduler.UnobservedTaskException and then ignored.

Are there problems if task completes normally, as nobody consumes its value?

Nope.

Follow-up: Is there a neat way to make sure task is awaited?

No.

Could a finally block be used to await task and re-throw potential exceptions?

Yes, if your code actually awaits the task. Presumably this would mean saving the task somewhere.

If task completed normally it would be awaited again in the finally block, but that shouldn't cause any problems?

You can await a task as many times as you like.

We could simply await task if we are not ready, however if there were 50 exit conditions this would be very tedious.

Then consider restructuring your code.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Huh. And here I thought such exceptions might even tear down the process. Would you then recommend leaving tasks potentially un-awaited (with `TaskScheduler.UnobservedTaskException` logging all exceptions), or should one strive to always consume their result? It doesn't sound as if there are any major downsides to it, assuming no exceptions are thrown "by design". – enzi Oct 12 '15 at 19:27
  • 2
    @enzi: It entirely depends on your application logic. If the operation is something that can be ignored, then it can be ignored. If not, then it shouldn't be ignored. – Stephen Cleary Oct 12 '15 at 22:12
  • 3
    Note that a `Task` can be awaited as many times as you like. However, a `ValueTask` can only be awaited once. – Dave Black Aug 28 '20 at 17:09
1

Follow-up: Is there a neat way to make sure task is awaited?

If you need individual, more fine-grained than TaskScheduler.UnobservedTaskException control over exceptions thrown by the tasks you don't await, there is a handy tool for that: async void methods.

Your code might look like this:

static async void Observe(Task task)
{        
    // use try/catch here if desired so;

    // otherwise, exceptions will be thrown out-of-band, i.e.
    // via SyncronizationContext.Post or 
    // via ThreadPool.QueueUSerWorkItem (if there's no sync. context) 

    await task; 
}

// ...

var taskObserved = false;
var task = DoSomething()
try
{
    bool ready = await DoSomethingElse();
    if (!ready) 
      return null;

    var value = await DoThirdThing(); // depends on DoSomethingElse
    taskObserved = true;
    return value + await task;
 }
 finally
 {
     if (!taskObserved)
        Observe(task);
 }

Some more details can be found here and here.

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • Is there a way to tell if the task has been observed yet or not, so I can choose whether to `await` it or not inside `Observe`? – avo Oct 30 '15 at 23:15
  • 1
    @avo, without resorting to reflection, I'm not aware of any documented way to check that. I suggest you ask this as a separate question though. – noseratio Oct 30 '15 at 23:23
1

The question: What happens to task if we're not ready and exit early?

The task will be ignored but you can also wind up with the following error if you have nested tasks beneath the task.

Message: Object reference not set to an instance of an object.

StackTrace:    at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.LegacyAspNetSynchronizationContext.CallCallbackPossiblyUnderLock(SendOrPostCallback callback, Object state)
   at System.Web.LegacyAspNetSynchronizationContext.CallCallback(SendOrPostCallback callback, Object state)
   at System.Threading.Tasks.AwaitTaskContinuation.RunCallback(ContextCallback callback, Object state, Task& currentTask)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
   at System.Threading.ThreadPoolWorkQueue.Dispatch()

This error will kill the process in a Web Application: NullReferenceException in System.Threading.Tasks calling HttpClient.GetAsync(url)

because TPL was trying to rejoin a Context that had been nulled out, so it was throwing a NullReferenceException outside of try/catch.

This is very hard to diagnose. In Production you won't see anything in try/catch, and in Visual Studio which await gets scheduled to rejoin the original context is somewhat random it depends on what the TaskScheduler happens to decide to do.

The general advice from Microsoft is that it is best practice to await all tasks:

As a best practice, you should always await the call.

If you want Fire and forget the recommended pattern is here: Simple Fire and Forget

Daniel Leach
  • 5,517
  • 4
  • 18
  • 32