11

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);
}
Andreas
  • 2,336
  • 2
  • 28
  • 45
  • Are you specifically talking about marshaling of the work back onto the UI thread? – Yuval Itzchakov Jan 18 '15 at 12:16
  • I now see that the question is not that specific as I thought, that it can be interpreted in different ways. I got better understanding reading your answer @YuvalItzchakov and after reading Sievajet's answer I see there is another answer when there's not an async IO bound operation. – Andreas Jan 18 '15 at 12:32
  • 1
    @Andreas I see you added another example and it works because again: there is no thread involved. Have you read into http://blog.stephencleary.com/2013/11/there-is-no-thread.html, as suggested by Yuval? – Krumelur Jan 18 '15 at 12:41
  • @Krumelur Yeah I did, and it makes perfect sense now when you pointed it out. A lot of reading right now. The brain is everywhere. Thanks! – Andreas Jan 18 '15 at 12:46
  • To learn more about `async-await`, read the articles on [my `async-await` curation](http://curah.microsoft.com/45553/asyncawait-general). – Paulo Morgado Jan 18 '15 at 22:48

3 Answers3

11

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:

Going down the async rabbit hole

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.

slugster
  • 49,403
  • 14
  • 95
  • 145
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • 1
    I don't think the OP is asking about the non-blocking I/O in the async method, I think he's asking about the fact that without using `ConfigureAwait(false)` he can effectively have multiple concurrent tasks accessing the current UI context (i.e. using the current UI thread). The answer is probably along the lines that concurrent tasks aren't necessarily executing on multiple different threads. – Ant P Jan 18 '15 at 12:12
  • @Andreas I might have misunderstood the question. Perhaps OP can clarify? – Yuval Itzchakov Jan 18 '15 at 12:13
  • I think I get it now. If we had done WriteToDisk in the sync way, we would have begun the operation and patiently waited for it to return. When doing theese things async we begin the operation (whatever type it is) walk away and let the operation (which often doesn't need a thread) tell us when it's done. Is it correctly (yet simplified) correct? – Andreas Jan 18 '15 at 12:15
  • 1
    @Andreas Yes, that is a very simplified version of how async IO works. Though if you're using a thread behind the scenes (Such as when invoking with `Task.Run`) then you are effectively consuming a threadpool thread, which as with async IO will marshal have its continuation marshaled to the right synchronization context if you `await` it. – Yuval Itzchakov Jan 18 '15 at 12:17
  • Really good explanation example and links to in depth articles. Thanks! – Andreas Jan 18 '15 at 13:02
  • @YuvalItzchakov What about Tasks that do run on a Threadpool thread how do they get pushed back to the ui thread , let me elaborate : In case of IO-Bound Operations an interrupt starts a process of queuing the callback to the UI-Thread . What queues a callback from a background thread ? – eran otzap Jan 18 '15 at 13:54
  • @eranotzap Same as for async IO bound operations. The sync context marshals them back via [`SynchronizationContext.Post`](http://msdn.microsoft.com/en-us/library/system.threading.synchronizationcontext.post%28v=vs.110%29.aspx) – Yuval Itzchakov Jan 18 '15 at 13:56
  • @YuvalItzchakov I keep earning the phrase Event/Message loop. For example of how Javascript works asynchronously . Does the CLR have an equivalent of that ? A kind of mechanism that runs and pulls Task status of running Tasks being awaited.. – eran otzap Jan 18 '15 at 13:59
  • A message loop is basically an endless loop which listens to events and executes them synchronously. This is how UI based applications work. See [this](http://stackoverflow.com/questions/2222365/what-is-a-message-pump) question for more details. The TPL based pattern doesn't have a notion of an event loop, it works via a state-machine pattern. – Yuval Itzchakov Jan 18 '15 at 14:19
  • @YuvalItzchakov: Technically, the DPC is part of the "response" mechanism. The OS sends the IRP to the driver, which then tells the device to do the work and then just returns (after marking the IRP as "pending"). – Stephen Cleary Jan 18 '15 at 19:30
4

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";
}
Sievajet
  • 3,443
  • 2
  • 18
  • 22
  • Hmm. Mayby I don't understand but in DoSomeAsyncWork I can still call an UI-element (say in a form application) and set values to it. How can that work if the code is marshalled back to the context first on await? – Andreas Jan 18 '15 at 12:25
  • You shouldnt be able to do that – Sievajet Jan 18 '15 at 12:29
  • So after the await your're returing to the synchronizationcontext it came from – Sievajet Jan 18 '15 at 12:36
  • So if the await is a non I/O or network operation, we DO use another thread? – Andreas Jan 18 '15 at 12:40
  • 1
    Task.Delay doesnt use a underlying thread to postpone further execution. using Task.Run(() => { }); does use a thread. – Sievajet Jan 18 '15 at 12:49
2

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

user2418209
  • 109
  • 8