How does running several tasks asynchronously on U

2019-01-23 07:10发布

问题:

I've read (and used) async/await quite a lot for some time now but I still have one question I can't get an answer to. Say I have this code.

private async void workAsyncBtn_Click(object sender, EventArgs e)
{
    var myTask = _asyncAwaitExcamples.DoHeavyWorkAsync(5);
    await myTask;
    statusTextBox.Text += "\r\n DoHeavyWorkAsync message";
}

It's called from the UI thread and returned to the UI Thread. Therefor I am able to do UI-specific things in this method and after the await myTask. If I had used .ConfigureAwait(false) I would get a thread exception when doing statusTextBox.Text += "\r\n DoHeavyWorkAsync message"; since I would have telled myTask it's ok to take any available thread from the thread pool.

My question. As I understand it I never leave the UI thread in this case, still it's run asynchronously, the UI is still responsive and I can start several Tasks at the same time and therefor speed up my application. How can this work if we only use one thread?

Thanks!

EDIT for Sievajet

private async void workAsyncBtn_Click(object sender, EventArgs e)
{
    await DoAsync();
}

private async Task DoAsync()
{
    await Task.Delay(200);
    statusTextBox.Text += "Call to form";
    await Task.Delay(200);
}

回答1:

As I understand it I never leave the UI thread in this case, still it's run asynchronously, the UI is still responsive and I can start several Tasks at the same time and therefor speed up my application. How can this work if we only use one thread?

First, i'd recommend reading Stephan Clearys blog post - There is no thread.

In order to understand how its possible to run multiple units of work altogether, we need to grasp one important fact: async IO bound operations have (almost) nothing to do with threads.

How is that possible? well, if we drill deep down all the way to the operating system, we'll see that the calls to the device drivers - those which are in charge of doing operations such as network calls and writing to disk, were all implemented as naturally asynchronous, they don't occupy a thread while doing their work. That way, while the device driver is doing its thing, there need not be a thread. only once the device driver completes its execution, it will signal the operating system that it's done via an IOCP (I/O completion port), which will then execute the rest of the method call (this is done in .NET via the threadpool, which has dedicated IOCP threads).

Stephans blog post demonstrates this nicely:

Once the OS executes the DPC (Deferred Procedure Call) and queue the IRP (I/O Request Packet), it's work is essentially done until the device driver signals it back with the I'm done messages, which causes a whole chain of operations (described in the blog post) to execute, which eventually will end up with invoking your code.

Another thing to note is that .NET does some "magic" for us behind the scenes when using async-await pattern. There is a thing called "Synchronization Context" (you can find a rather lengthy explanation here). This sync context is whats in-charge of invoking the continuation (code after the first await) on the UI thread back again (in places where such context exists).

Edit:

It should be noted that the magic with the synchronization context happens for CPU bound operations as well (and actually for any awaitable object), so when you use a threadpool thread via Task.Run or Task.Factory.StartNew, this will work as well.



回答2:

The TaskParallelLibrary (TPL) uses a TaskScheduler which can be configured with TaskScheduler.FromCurrentSynchronizationContext to return to the SynchronizationContext like this :

textBox1.Text = "Start";
// The SynchronizationContext is captured here
Factory.StartNew( () => DoSomeAsyncWork() )
.ContinueWith( 
    () => 
    {
       // Back on the SynchronizationContext it came from            
        textBox1.Text = "End";
    },TaskScheduler.FromCurrentSynchronizationContext());

When an async method suspends at an await, by default it will capture the current SynchronizationContext and marshall the code after the await back on the SynchronizationContext it came from.

        textBox1.Text = "Start";

        // The SynchronizationContext is captured here

       /* The implementation of DoSomeAsyncWork depends how it runs, this could run on the threadpool pool 
          or it could be an 'I/O operation' or an 'Network operation' 
          which doesnt use the threadpool */
        await DoSomeAsyncWork(); 

        // Back on the SynchronizationContext it came from
        textBox1.Text = "End";

async and await example:

async Task MyMethodAsync()
{
  textBox1.Text = "Start";

  // The SynchronizationContext is captured here
  await Task.Run(() => { DoSomeAsyncWork(); }); // run on the threadPool

  // Back on the SynchronizationContext it came from
  textBox1.Text = "End";
}


回答3:

When UI thread calls await it starts the async operation and returns immediately. When the async operation completes, it notifies a thread from the thread pool but the internal implementation of async await dispatches the execution to the UI thread which will continue the execution of the code after the await.

The Dispatch is implemented by means of SynchronizationContext which in turn calls System.Windows.Forms.Control.BeginInvoke.

CLR via C# (4th Edition) (Developer Reference) 4th Edition by Jeffrey Richter page 749

Actually, Jeffrey worked with MS to implement the async/await inspired by his AsyncEnumerator