2

What's the difference between starting and awaiting? Code below taken from Stephen Cleary's blog (including comments)

public async Task DoOperationsConcurrentlyAsync()
{
  Task[] tasks = new Task[3];
  tasks[0] = DoOperation0Async();
  tasks[1] = DoOperation1Async();
  tasks[2] = DoOperation2Async();

  // At this point, all three tasks are running at the same time.

  // Now, we await them all.
  await Task.WhenAll(tasks);
}

I thought that the tasks begin running when you await them ... but the comments in the code seem to imply otherwise. Also, how can the tasks be running after I just attributed them to an array of type Task. Isn't that just an attribution, by nature not involving action?

trashr0x
  • 6,457
  • 2
  • 29
  • 39
Sami
  • 113
  • 3
  • 12
  • 1
    @janeE - you should link to the exact Stephen Cleary article, and/or include an outline of the DoOperation#Async() here. – H H Oct 21 '18 at 21:05
  • I removed my comment, because I/it was wrong. Thanks @HenkHolterman – Dennis Kuypers Oct 21 '18 at 23:14
  • I've answered this with code and explanation at this link. https://stackoverflow.com/questions/51757901/why-await-an-async-function-directly-not-work-without-assigning-it-to-a-task-var/51757945#51757945 – Michael Puckett II Oct 22 '18 at 02:59
  • @HenkHolterman I was hoping you would comment on my answer. I'd appreciate the feedback – Dennis Kuypers Oct 23 '18 at 09:52

4 Answers4

6

A Task returns "hot" (i.e. already started). await asynchronously waits for the Task to complete.

In your example, where you actually do the await will affect whether the tasks are ran one after the other, or all of them at the same time:

await DoOperation0Async(); // start DoOperation0Async, wait for completion, then move on
await DoOperation1Async(); // start DoOperation1Async, wait for completion, then move on
await DoOperation2Async(); // start DoOperation2Async, wait for completion, then move on

As opposed to:

tasks[0] = DoOperation0Async(); // start DoOperation0Async, move on without waiting for completion
tasks[1] = DoOperation1Async(); // start DoOperation1Async, move on without waiting for completion
tasks[2] = DoOperation2Async(); // start DoOperation2Async, move on without waiting for completion

await Task.WhenAll(tasks); // wait for all of them to complete

Update

"doesn't await make an async operation... behave like sync, in this example (and not only)? Because we can't (!) run anything else in parallel with DoOperation0Async() in the first case. By comparison, in the 2nd case DoOperation0Async() and DoOperation1Async() run in parallel (e.g. concurrency,the main benefits of async?)"

This is a big subject and a question worth being asked as it's own thread on SO as it deviates from the original question of the difference between starting and awaiting tasks - therefore I'll keep this answer short, while referring you to other answers where appropriate.

No, awaiting an async operation does not make it behave like sync; what these keywords do is enabling developers to write asynchronous code that resembles a synchronous workflow (see this answer by Eric Lippert for more).

Calling await DoOperation0Async() will not block the thread executing this code flow, whereas a synchronous version of DoOperation0 (or something like DoOperation0Async.Result) will block the thread until the operation is complete.

Think about this in a web context. Let's say a request arrives in a server application. As part of producing a response to that request, you need to do a long-running operation (e.g. query an external API to get some value needed to produce your response). If the execution of this long-running operation was synchronous, the thread executing your request would block as it would have to wait for the long-running operation to complete. On the other hand, if the execution of this long-running operation was asynchronous, the request thread could be freed up so it could do other things (like service other requests) while the long-running operation was still running. Then, when the long-running operation would eventually complete, the request thread (or possibly another thread from the thread pool) could pick up from where it left off (as the long-running operation would be complete and it's result would now be available) and do whatever work was left to produce the response.

The server application example also addresses the second part of your question about the main benefits of async - async/await is all about freeing up threads.

trashr0x
  • 6,457
  • 2
  • 29
  • 39
  • thashr0x thank you, your answer really is the easiest to follow, straight to the point and I appreciate that you resist the temptation to talk about other related things that I am not advanced enough at this point to understand, so I wanted to confirm the following: doesn't await make an async operation... behave like sync, in this example (and not only)? Because we can't (!) run anything else in parallel with DoOperation0Async() in the first case. By comparison, in the 2nd case DoOperation0Async() and DoOperation1Async() run in parallel (e.g. concurrency,the main benefits of async?) – Sami Oct 27 '18 at 16:57
  • trashr0x, I am checking Eric's answer. To the question "To me, it'll kill performance from spinning up all those threads, no?" , he answers: "There are no threads. Concurrency is a mechanism for achieving asynchrony; it is not the only one." - do you agree with that? – Sami Oct 31 '18 at 17:17
  • AFAIK, concurrency is concerned with managing access to shared state from different threadS, therefore the answer should be: "It's not *required* that there are multiple threads (although there can be). Concurrency is a mechanism for achieving asynchrony; it is not the only one." – Sami Oct 31 '18 at 17:19
1

Isn't that just an attribution, by nature not involving action?

By calling the async method you execute the code within. Usually down the chain one method will create a Task and return it either by using return or by awaiting.

Starting a Task

You can start a Task by using Task.Run(...). This schedules some work on the Task Thread Pool.

Awaiting a Task

To get a Task you usually call some (async) Method that returns a Task. An async method behaves like a regular method until you await (or use Task.Run() ). Note that if you await down a chain of methods and the "final" method only does a Thread.Sleep() or synchronous operation - then you will block the initial calling thread, because no method ever used the Task's Thread Pool.

You can do some actual asynchronous operation in many ways:

These are the ones that come to my mind, there are probably more.

By example

Let's assume that Thread ID 1 is the main thread where you are calling MethodA() from. Thread IDs 5 and up are Threads to run Tasks on (System.Threading.Tasks provides a default Scheduler for that).

public async Task MethodA()
{
    // Thread ID 1, 0s passed total
    var a = MethodB(); // takes 1s
    // Thread ID 1, 1s passed total
    await Task.WhenAll(a); // takes 2s
    // Thread ID 5, 3s passed total

    // When the method returns, the SynchronizationContext
    // can change the Thread - see below
}

public async Task MethodB()
{
    // Thread ID 1, 0s passed total
    Thread.Sleep(1000); // simulate blocking operation for 1s
    // Thread ID 1, 1s passed total

    // the await makes MethodB return a Task to MethodA
    // this task is run on the Task ThreadPool
    await Task.Delay(2000); // simulate async call for 2s
    // Thread ID 2 (Task's pool Thread), 3s passed total
}

We can see that MethodA was blocked on the MethodB until we hit an await statement.

Await, SynchronizationContext, and Console Apps

You should be aware of one feature of Tasks. They make sure to invoke back to a SynchronizationContext if one is present (basically non-console apps). You can easily run into a deadlock when using .Result or .Wait() on a Task if the called code does not take measures. See https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/

async/await as syntactic sugar

await basically just schedules following code to run after the call was completed. Let me illustrate the idea of what is happening behind the scenes.

This is the untransformed code using async/await. The Something method is awaited, so all following code (Bye) will be run after Something completed.

public async Task SomethingAsync()
{
    Hello();
    await Something();
    Bye();
}

To explain this I add a utility class Worker that simply takes some action to run and then notify when done.

public class Worker
{
    private Action _action;

    public event DoneHandler Done;
    // skipping defining DoneHandler delegate

    // store the action
    public Worker(Action action) => _action = action;

    public void Run()
    {
        // execute the action
        _action();

        // notify so that following code is run
        Done?.Invoke();
    }
}

Now our transformed code, not using async/await

public Task SomethingAsync()
{
    Hello(); // this remains untouched

    // create the worker to run the "awaited" method
    var worker = new Worker(() => Something());

    // register the rest of our method
    worker.Done += () => Bye();

    // execute it
    worker.Run();

    // I left out the part where we return something
    // or run the action on a threadpool to keep it simple        
}
Dennis Kuypers
  • 546
  • 4
  • 16
0

Here's the short answer:

To answer this you just need to understand what the async / await keywords do.

We know a single thread can only do one thing at a time and we also know that a single thread bounces all over the application to various method calls and events, ETC. This means that where the thread needs to go next is most likely scheduled or queued up somewhere behind the scenes (it is but I won't explain that part here.) When a thread calls a method, that method is ran to completion before any other methods can be ran which is why long running methods are preferred to be dispatched to other threads to prevent the application from freezing. In order to break a single method up into separate queues we need to do some fancy programming OR you can put the async signature on the method. This tells the compiler that at some point the method can be broken up into other methods and placed in a queue to be ran later.

If that makes sense then you're already figuring out what await does... await tells the compiler that this is where the method is going to be broken up and scheduled to run later. This is why you can use the async keyword without the await keyword; although the compiler knows this and warns you. await does all this for you by use of a Task.

How does await use a Task tell the compiler to schedule the rest of the method? When you call await Task the compilers calls the Task.GetAwaiter() method on that Task for you. GetAwaiter() return a TaskAwaiter. The TaskAwaiter implements two interfaces ICriticalNotifyCompletion, INotifyCompletion. Each has one method, UnsafeOnCompleted(Action continuation) and OnCompleted(Action continuation). The compiler then wraps the rest of the method (after the await keyword) and puts it in an Action and then it calls the OnCompleted and UnsafeOnCompleted methods and passes that Action in as a parameter. Now when the Task is complete, if successful it calls OnCompleted and if not it calls UnsafeOnCompleted and it calls those on the same thread context used to start the Task. It uses the ThreadContext to dispatch the thread to the original thread.

Now you can understand that neither async or await execute any Tasks. They simply tell the compiler to use some prewritten code to schedule all of it for you. In fact; you can await a Task that's not running and it will await until the Task is executed and completed or until the application ends.

Knowing this; lets get hacky and understand it deeper by doing what async await does manually.


Using async await

using System;
using System.Threading.Tasks;
namespace Question_Answer_Console_App
{
    class Program
    {
        static void Main(string[] args)
        {
            Test();
            Console.ReadKey();
        }

        public static async void Test()
        {
            Console.WriteLine($"Before Task");
            await DoWorkAsync();
            Console.WriteLine($"After Task");
        }

        static public Task DoWorkAsync()
        {
            return Task.Run(() =>
            {
                Console.WriteLine($"{nameof(DoWorkAsync)} starting...");
                Task.Delay(1000).Wait();
                Console.WriteLine($"{nameof(DoWorkAsync)} ending...");
            });
        }
    }
}
//OUTPUT
//Before Task
//DoWorkAsync starting...
//DoWorkAsync ending...
//After Task

Doing what the compiler does manually (sort of)

Note: Although this code works it is meant to help you understand async await from a top down point of view. It DOES NOT encompass or execute the same way the compiler does verbatim.

using System;
using System.Threading.Tasks;
namespace Question_Answer_Console_App
{
    class Program
    {
        static void Main(string[] args)
        {
            Test();
            Console.ReadKey();
        }

        public static void Test()
        {
            Console.WriteLine($"Before Task");
            var task = DoWorkAsync();
            var taskAwaiter = task.GetAwaiter();
            taskAwaiter.OnCompleted(() => Console.WriteLine($"After Task"));
        }

        static public Task DoWorkAsync()
        {
            return Task.Run(() =>
            {
                Console.WriteLine($"{nameof(DoWorkAsync)} starting...");
                Task.Delay(1000).Wait();
                Console.WriteLine($"{nameof(DoWorkAsync)} ending...");
            });
        }
    }
}
//OUTPUT
//Before Task
//DoWorkAsync starting...
//DoWorkAsync ending...
//After Task

LESSON SUMMARY:

Note that the method in my example DoWorkAsync() is just a function that returns a Task. In my example the Task is running because in the method I use return Task.Run(() =>…. Using the keyword await does not change that logic. It's exactly the same; await only does what I mentioned above.

If you have any questions just ask and I'll be happy to answer them.

Michael Puckett II
  • 6,586
  • 5
  • 26
  • 46
  • tl;dr. I didn't read it all but the code samples are off. You should not use `async void` here, reserve that for event handlers. You should not (almost never) call `.Wait()`. You do not need `Task.Run()` to show asynchronosity, the Task.Delay() call would be enough here. – H H Oct 22 '18 at 05:29
  • _"This is NOT what you want to happen!!"_ - O no? Why not? – H H Oct 22 '18 at 16:54
  • Async/await is very much a bridge technology. It matters a lot whether your frontend is Console, WinForms or ASP.NET. And even more if the backend is CPU or I/O intensive. Every combination has its ow list of DOs, DON'Ts and best practices. – H H Oct 22 '18 at 16:56
  • 1
    Your only example is Console with Task.Delay (simulation of CPU) and for that scenario you have almost everything wrong. – H H Oct 22 '18 at 16:57
  • Make it `Task Test() {}` and then call `Test.Wait();` in Main(). That is one of the few uses of Wait() that are okay. Also get rid of Task.Run(). – bommelding Oct 23 '18 at 07:26
  • @bommelding No, I don't want to wait in main. I want to be able to hit the key and end the app. The way it is designed is correct. Also, why get rid of Task.Run? – Michael Puckett II Oct 23 '18 at 11:00
  • That is kind of a strange requirement, but possible. That still leaves the Task.Run(), it can and should go. You get an implicit thread in the await, when you make it properly async. That Delay().Wait() is an eyesore now. – bommelding Oct 24 '18 at 06:47
  • You lost me. How would you execute the new task? Not the Delay.Wait, the Task.Run. Task.Run is the preferred shortcut as opposed to using the Task.Factory.StartNew – Michael Puckett II Oct 24 '18 at 11:37
  • Also, the removal of Wait in main is not a strange requirement but a needed requirement, especially in this example. If you need to block main for another operation then that operation should not be in a separate thread unless you can't avoid it, meaning it's out of your control. Don't block main with a wait.. what purpose have you served and what have you gained? Simply run the logic inline. This example was designed completely to show and explain some. of what the async and await keywords do and the example itself shows await auto wiring the OnCompleted method. – Michael Puckett II Oct 24 '18 at 11:43
  • As far as the Task.Delay.Wait I could have made the Task itself async and awaited the Delay but you then spin up a new Task. This is ideal to prevent deadlocks and keep order but not if you know your task is a separate thread and it is not sharing resources. it's a general rule not to do it but in this example. it is completely justified and although the performance doesn't matter it is more efficient. Knowing when to and when not to is more important than the rule but if you don't know you follow the general guideline rule. – Michael Puckett II Oct 24 '18 at 11:51
  • Thank you Michael, is it correct that await DoOperation0Async(); starts DoOperation0Async, waits for completion, then moves on; and doing this assignment: tasks[0] = DoOperation0Async(); it means we move on without waiting for completion? In this sense, doesn't await make an async operation sync? Because we can't run anything else in parallel with DoOperation0Async() in the first case, since "we are waiting for completion". Thanks – Sami Oct 27 '18 at 16:51
  • I'm not sure what you're referring to with parallism at the end but the beginning is correct. You start the async operation by calling it regardless of await and you wait for it when you add the await keyword (in a nutshell) – Michael Puckett II Oct 28 '18 at 17:22
-6

With starting you start a task. That means it might be picked up for execution by whatever Multitasaking system is in place.

With waiting, you wait for one task to actually finish before you continue.

There is no such thing as a Fire and Forget Thread. You always need to come back, to react to exceptions or do somethings with the result of the asynchronous operation (Database Query or WebQuery result, FileSystem operation finished, Dokument send to the nearest printer pool).

You can start and have as many task running in paralell as you want. But sooner or later you will require the results before you can go on.

Christopher
  • 9,634
  • 2
  • 17
  • 31
  • I’m really interested to hear why there isn’t fire and forget tasks ever. Everyone else seems to disagree. And often there are no results so there’s nothing to wait on. Also there are limits on parallel tasks. – Sami Kuhmonen Oct 21 '18 at 20:55
  • @SamiKuhmonen: 'Maxim 26. "Fire and Forget" is fine, provided you never actually forget.' One of the biggest sins in exception handling, is to swallow exceptions. Particular Fatal ones. In most cases you have to write faulty exception handling code for that. But with Multithreading and Multitasking? You have to write extra code to not swallow them. It is more of a problem with Threads then with tasks. But Tasks are inherently agnostic towards how they are executed: Multitasking, Multithreading, Threadpool, a combination of those. Best to aim for the worst case. – Christopher Oct 21 '18 at 21:05
  • An `async void` is fire and forget. Which can be bad if it is not totally handling all its own exceptions. Same for threads, they are always fire-and-forget as seen from the spawning thread. – H H Oct 21 '18 at 21:11
  • @HenkHolterman I repeat: 'Maxim 26. "Fire and Forget" is fine, provided you never **actually** forget.' – Christopher Oct 21 '18 at 22:07
  • 4
    What is “Maxim 26”? – Sami Kuhmonen Oct 22 '18 at 04:43
  • @SamiKuhmonen: https://en.wikipedia.org/wiki/Schlock_Mercenary#The_Seventy_Maxims_of_Maximally_Effective_Mercenaries For some reason, that one really fits for Multithreading. – Christopher Oct 22 '18 at 13:44
  • I assumed there would be some actual reference or argument that relates to the matter but I didn’t expect that. Speechless. – Sami Kuhmonen Oct 22 '18 at 13:50
  • @SamiKuhmonen: The actuall argument was **literally** the entirety of the post and the exchange in comments. If you do not agree on something as simple as "swallowing exceptions is bad", then I am speechless what to tell you. – Christopher Oct 22 '18 at 14:27
  • @Christopher Your answer doesn't say, "swallowing exceptions is bad". Your answer says, "It's impossible to start a task without observing the result". It's trivial to demonstrate that it's possible. You can argue that it's not a good idea, which people may or may not agree with, but saying it's not possible is just demonstrably false. – Servy Oct 22 '18 at 15:04
  • @Servy: **Literally** the 2nd sentence of the first comment reply: "One of the biggest sins in exception handling, is to swallow exceptions." – Christopher Oct 22 '18 at 15:06
  • Which isn't in your answer, like I said. And no one said that exceptions should be swallowed; there was no one contradicting that part of that comment. Everyone is just telling you that your answer stating it's not possible to have a fire and forget task is wrong. – Servy Oct 22 '18 at 15:09