0

I have an async method, which calls another async method, however, I want it run on a separate thread in parallel:

public async Task<Page> ServePage() {
  Task.Run(() => DoThings(10));   // warning here

  // ... other code
  return new Page();
}

public async Task DoThings(int foo) {
  // stuff
}

The warning states:

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

That is, in fact, what I am trying to do. Why am I getting a compiler warning? Is the syntax of Task.Run incorrect?

AngryHacker
  • 59,598
  • 102
  • 325
  • 594
  • 1
    Read this link I think its helpful: https://stackoverflow.com/questions/34680985/what-is-the-difference-between-asynchronous-programming-and-multithreading – hassan.ef Apr 25 '19 at 18:02
  • 2
    `Task.Run` returns an object that you can await. It is recommended that you await it because that is the only way to observe any exception on that thread. If an exception goes unobserved, it can crash your application. Awaiting the task won’t block execution. – John Wu Apr 25 '19 at 18:13

4 Answers4

6

TL;DR

The reason why you get the warning is because

 Task.Run(() => DoThings(10));   // warning here

returns a Task, and since your ServePage method is marked as async, the compiler believes that you should await the result of the Task

Detail

You're mixing two very different paradigms, which coincidentally both involve Task, viz:

  • Task.Run(), which is generally useful for parallelizing CPU bound work by utilizing multiple cores which may be available
  • async / await, which is useful for waiting for I/O bound operations to complete, without blocking (wasting) a thread.

So for instance, if you wanted to do 3 x CPU bound operations concurrently, and since Task.Run returns a Task, what you could do is:

public Page ServePage() // If we are CPU bound, there's no point decorating this as async
{
    var taskX = Task.Run(() => CalculateMeaningOfLife()); // Start taskX
    var taskY = Task.Run(() => CalculateJonSkeetsIQ()); // Start taskY
    var z = DoMoreHeavyLiftingOnCurrentThread();
    Task.WaitAll(taskX, taskY); // Wait for X and Y - the Task equivalent of `Thread.Join`
    
    // Return a final object comprising data from the work done on all three tasks
    return new Page(taskX.Result, taskY.Result, z);
}

The above is likely to utilise up to three threads, which could do the CPU bound work concurrently if there are sufficient cores to do so. Note however that using multiple threads concurrently will reduce the scalability of your system, since fewer simultaneous pages can be served without context switching.

This is in contrast to async / await, which is generally used to free up threads while waiting for I/O bound calls to complete. Async is commonly used in Api and Web apps to increase scalability as the thread is released for other work while the IO bound work happens. Assuming DoThings is indeed I/O bound, we can do something like:

public async Task<string> DoThings(int foo) {
   var result = await SomeAsyncIo(foo);
   return "done!";
}

Async work can also be done in parallel:

public async Task<Page> ServePage() {
   var task1 = DoThings(123); // Kick off Task 1
   var task2 = DoThings(234); // Kick off Task 2 in parallel with task 1
   await Task.WhenAll(task1, task2); // Wait for both tasks to finish, while releasing this thread
   return new Page(task1.Result, task2.Result); // Return a result with data from both tasks
}

If the I/O bound work takes a reasonable amount of time, there's a good chance there's a point during the await Task.WhenAll when ZERO threads are actually running - See Stephen Cleary's article.

There's a 3rd, but very dangerous option, which is fire and forget. Since method DoThings is already marked as async, it already returns a Task, so there's no need at all to use Task.Run at all. Fire and forget would look as follows:

public Page ServePage() // No async
{
  #pragma warning disable 4014 //   warning is suppresed by the Pragma
  DoThings(10);   // Kick off DoThings but don't wait for it to complete.
  #pragma warning enable 4014

  // ... other code
  return new Page();
}

As per @JohnWu's comment, the 'fire and forget' approach is dangerous and usually indicates a design smell. More on this here and here

Edit

Re:

there is nuance to this that escapes me over and over, such as that calling an async method that returns Task from a synchronous method fires-and-forgets execution of the method. (That's the very last code sample.) Am I understanding that correctly?

It's a bit difficult to explain, but irrespective of whether called with, or without the await keyword, any synchronous code in an invoked async method before the first await will be executed on the caller's thread, unless we resort to hammers like Task.Run.

Perhaps this example might help the understanding (note that we're deliberately using synchronous Thread.Sleep and not await Task.Delay to simulate CPU bound work and introduce latency which can be observed)

public async Task<Page> ServePage()
{
  // Launched from this same thread, 
  // returns after ~2 seconds (i.e. hits both sleeps) 
  // continuation printed.
  await DoThings(10);

  #pragma warning disable 4014
  // Launched from this same thread, 
  // returns after ~1 second (i.e. hits first sleep only) 
  // continuation not yet printed
  DoThings(10);   

  // Task likely to be scheduled on a second thread
  // will return within few milliseconds (i.e. not blocked by any sleeps)
  Task.Run(() => DoThings(10));   

  // Task likely to be scheduled on a second thread
  // will return after 2 seconds, although caller's thread will be released during the await
  // Generally a waste of a thread unless also doing CPU bound work on current thread, or unless we want to release the calling thread.
  await Task.Run(() => DoThings());   

  // Redundant state machine, returns after 2 seconds
  // see return Task vs async return await Task https://stackoverflow.com/questions/19098143
  await Task.Run(async () => await DoThings());   
}

public async Task<string> DoThings(int foo) {
   Thread.Sleep(1000); 
   var result = await SomeAsyncIo(foo);
   Trace.WriteLine("Continuation!"); 
   Thread.Sleep(1000); 
   return "done!";
}

There's one other important point to note - in most cases, there are no guarantees that the continuation code AFTER an await will be executed on the same thread as that before the await. The continuation code is re-written by the compiler into a Task, and the continuation task will be scheduled on the thread pool.

StuartLC
  • 104,537
  • 17
  • 209
  • 285
  • As much as I learn (or think I learn) there is nuance to this that escapes me over and over, such as that calling an `async` method that returns `Task` from a synchronous method fires-and-forgets execution of the method. (That's the very last code sample.) Am I understanding that correctly? – Scott Hannen Apr 25 '19 at 20:54
  • ...however I'm unable to confirm this behavior. When I test a synchronous method that calls an async method that returns Task, the method executes synchronously, which is what I would have expected, even though I still get the compiler warning. But if I use Task.Run (as in the original post) it executes on another thread and the method that calls it resumes immediately (also what I expected.) – Scott Hannen Apr 25 '19 at 21:08
  • The ServePage method is largely composed of `await` statements. There is one condition where it makes sense to take a single long-lived (both CPU and I/O bound) operation and do it in parallel. So the `DoThings()` method is in an `if` block. At least logically, it makes sense to run it concurrently to ServePage (since it actually lasts longer than ServePage). – AngryHacker Apr 25 '19 at 22:26
  • @AngryHacker I've tried to answer your question with an example. – StuartLC Apr 26 '19 at 06:54
1

This

Task.Run(() => DoThings(10));

will run your separate task "in parallel", meaning that another thread will run this task. The thread that entered this method will then continue execution of the next statement.

What you're doing here is allowed. That's why it's a warning. (I'm assuming that the rest of the method, not shown, returns a Page.)

The message is warning you that because the other task is executing on another thread, it could execute before, after, or at the same time as the other code in the method. This method no longer "knows" what the task is doing or when it's finished.

It's saying, in other words:

Don't assume that because this line of code appears before other lines of code in the method that it's going to execute before they do. If something needs to execute after this task finishes, await it before doing the next thing.

As an example:

public int DoSomething()
{
    var x = 1;
    Task.Run(() => x++);
    return x;        
}

What does this return? It depends. It could return 1 or 2. It may return either before or after x is incremented. If you care whether or not x has been incremented then this is bad. If your task does something that doesn't matter to the rest of the method and you don't care whether the task even finishes before or after the rest of the method, then this warning wouldn't matter to you. If you do care then it's important and you'd want to await the task.

Scott Hannen
  • 27,588
  • 3
  • 45
  • 62
  • would `_ = Task.Run(() => DoThings(10));` achieve what I want? It's returning a Task into an empty variable that I don't care about (because Fire and Forget), thus satisfying the compiler. – AngryHacker Apr 25 '19 at 19:14
  • No, I don't think that would make any difference. It's also two different things. If it's doing what you want then the compiler warning doesn't apply in this case and you can suppress it with a comment. As mentioned in another answer, there are other risks involved. For example, when we do this sort of "fire and forget" exceptions are no longer raised to the calling method. – Scott Hannen Apr 25 '19 at 21:26
  • Which is fine. The FireAndForgetMethod has it's own error handling. – AngryHacker Apr 25 '19 at 22:20
1

All versions bellow are valid, warning free, and more or less equivalent:

public Task ServePage1()
{
    return Task.Run(async () => await DoThings(10));
}

public async Task ServePage2()
{
    await Task.Run(async () => await DoThings(10));
}

public Task ServePage3()
{
    return Task.Run(() => DoThings(10));
}

public async Task ServePage4()
{
    await Task.Run(() => DoThings(10));
}

In general you should not fire-and-forget tasks by ignoring the return value of Task.Run. If you do that the compiler will issue a warning, because it is rarely intentional.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
1

When you call an async method that returns a Task, you can do two things:

  1. Await the Task, which returns control to the caller immediately, and resumes when the Task completes (with or without a return value). This is also where you can trap any exceptions that might have happened when executing the Task.

  2. Fire and Forget. This is when you start a Task and completely cut it loose - including the ability to trap exceptions. Most importantly, unless awaited, control execution will proceed beyond the call and can cause unintended state corruption issues.

You did #2 in your code. While technically allowed, because of the reasons cited above, the compiler warned you.

Question for you though - if you really just wanted to fire and forget, why did you need to create that explicit Task? You could just have called DoThings(10) directly, couldn't you? Unless I am missing something in the code that is not visible to me. So - couldn't you have done just this?

public async Task<Page> ServePage() {
  DoThings(10); 
}
soumyasg
  • 33
  • 5