简体   繁体   中英

async Task - What actually happens on the CPU?

I've been reading about Task s after asking this question and seeing that I completely misunderstood the concept. Answers such as the top answers here and here explain the idea , but I still don't get it. So I've made this a very specific question: What actually happens on the CPU when a Task is executed?

This is what I've understood after some reading: A Task will share CPU time with the caller (and let's assume the caller is the "UI") so that if it's CPU-intensive - it will slow down the UI. If the Task is not CPU-intensive - it will be running "in the background". Seems clear enough …… until tested. The following code should allow the user to click on the button, and then alternately show "Shown" and "Button". But in reality: the Form is completely busy (-no user input possible) until the "Shown"s are all shown.

public Form1()
{
    InitializeComponent();
    Shown += Form1_Shown;
}

private async void Form1_Shown(object sender, EventArgs e)
{
    await Doit("Shown");
}

private async Task Doit(string s)
{
    WebClient client = new WebClient();
    for (int i = 0; i < 10; i++)
    {
        client.DownloadData(uri);//This is here in order to delay the Text writing without much CPU use.
        textBox1.Text += s + "\r\n";
        this.Update();//textBox1.
    }
}

private async void button1_Click(object sender, EventArgs e)
{
    await Doit("Button");
}

Can someone please tell me what is actually happening on the CPU when a Task is executed (eg "When the CPU is not used by the UI, the Task uses it, except for when… etc.")?

The key to understanding this is that there are two kinds of tasks - one that executes code (what I call Delegate Tasks), and one that represents a future event (what I call Promise Tasks). Those two tasks are completely different, even though they're both represented by an instance of Task in .NET. I have some pretty pictures on my blog that may help understand how these types of task are different.

Delegate Tasks are the ones created by Task.Run and friends. They execute code on the thread pool (or possibly another TaskScheduler if you're using a TaskFactory ). Most of the "task parallel library" documentation deals with Delegate Tasks. These are used to spread CPU-bound algorithms across multiple CPUs, or to push CPU-bound work off a UI thread.

Promise Tasks are the ones created by TaskCompletionSource<T> and friends (including async ). These are the ones used for asynchronous programming, and are a natural fit for I/O-bound code.

Note that your example code will cause a compiler warning to the effect that your "asynchronous" method Doit is not actually asynchronous but is instead synchronous. So as it stands right now, it will synchronously call DownloadData , blocking the UI thread until the download completes, and then it will update the text box and finally return an already-completed task.

To make it asynchronous, you have to use await :

private async Task Doit(string s)
{
  WebClient client = new WebClient();
  for (int i = 0; i < 10; i++)
  {
    await client.DownloadDataTaskAsync(uri);
    textBox1.Text += s + "\r\n";
    this.Update();//textBox1.
  }
}

Now it's returning an incomplete task when it hits the await , which allows the UI thread to return to its message processing loop. When the download completes, the remainder of this method will be queued to the UI thread as a message, and it will resume executing that method when it gets around to it. When the Doit method completes, then the task it returned earlier will complete.

So, tasks returned by async methods logically represent that method. The task itself is a Promise Task, not a Delegate Task, and does not actually "execute". The method is split into multiple parts (at each await point) and executes in chunks, but the task itself does not execute anywhere.

For further reading, I have a blog post on how async and await actually work (and how they schedule the chunks of the method) , and another blog post on why asynchronous I/O tasks do not need to block threads .

Tasks use the ThreadPool you can read extensively about what it is and how it works here

But in a nutshell, when a task is executed, the Task Scheduler looks in the ThreadPool to see if there is a thread available to run the action of the task. If not, it's going to be queued until one becomes available.

A ThreadPool is just a collection of already-instantiated threads made available so that multithreaded code can safely use concurrent programming without overwhelming the CPU with context-switching all the time.

Now, the problem with your code is that even though you return an object of type Task , you are not running anything concurrently - No separate thread is ever started!

In order to do that, you have two options, either you start your Doit method as a Task, with

Option1

Task.Run(() => DoIt(s));

This will run the whole DoIt method on another thread from the Thread Pool, but it will lead to more problems, because in this method, you're trying to access UI-controls. therefore, you will need either to marshal those calls to the UI thread, or re-think your code so that the UI access is done directly on the UI thread after the asynchronous tasks completes.


Option 2 (preferred, if you can)

You use .net APIs which are already asynchronous, such as client.DownloadDataTaskAsync(); instead of client.DownloadData();

now, in your case, the problem is that you will need to have 10 calls, which are going to return 10 different objects of type Task<byte[]> and you want to await on the completion of all of them, not just one.

In order to do this, you will need to create a List<Task<byte[]>> returnedTasks and you will add to it all returned value from DownloadDataTaskAsync() . then, once this is done, you can use the following return value for your DoIt method.

return Task.WhenAll(returnedTasks);

As per your linked answers, Tasks and Threads are totally different concepts, and you are also getting confused with async / await

A Task is just a representation of some work to be done. It says nothing about HOW that work should be done.

A Thread is a representation of some work that is running on the CPU, but is sharing the CPU time with other threads that it can know nothing about.

You can run a Task on a Thread using Task.Run() . Your Task will run asynchronously and independently of any other code providing a threadpool thread is available.

You can also run a Task asynchronously on the SAME thread using async / await. Anytime the thread hits an await, it can save the current stack state, then travel back up the stack and carry on with other work until the awaited task has finished. Your Doit() code never awaits anything, so will run synchronously on your GUI thread until complete.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM