1

SO I've been doing a ton of reading lately about the net Async CTP and one thing that keeps coming up are sentences like these: "Asynchrony is not about starting new threads, its about multiplexing work", and "Asynchrony without multithreading is the same idea [as cooperative multitasking]. You do a task for a while, and when it yields control, you do another task for a while on that thread".

I'm trying to understand whether remarks like this are purely (well, mostly) esoteric and academic or whether there is some language construct I'm overlooking that allows the tasks I start via "await" to magically run on the UI thread.

In his blog, Eric Lippert gives this example as a demonstration of how you can have Asyncrhony without multithreading:

async void FrobAll()
{
    for(int i = 0; i < 100; ++i)
    {
        await FrobAsync(i); // somehow get a started task for doing a Frob(i) operation on this thread
    }
} 

Now, it's the comment that intrigues me here: "...get a started task for doing a Frob(i) operation on this thread".

Just how is this possible? Is this a mostly theoretical remark? The only cases I can see so far where a task appears to not need a separate thread (well, you can't know for sure unless you examine the code) would be something like Task.Delay(), which one can await on without starting another thread. But I consider this a special case, since I didn't write the code for that.

For the average user who wants to offload some of their own long running code from the GUI thread, aren't we talking primarily about doing something like Task.Run to get our work offloaded, and isn't that going to start it in another thread? If so, why all this arm waiving about not confusing asynchorny with multithreading?

Michael Ray Lovett
  • 6,668
  • 7
  • 27
  • 36

3 Answers3

2

Please see my intro to async/await.

In the case of CPU work, you do have to use something like Task.Run to execute it in another thread. However, there's a whole lot of work which is not CPU work (network requests, file system requests, timers, ...), and each of those can be wrapped in a Task without using a thread.

Remember, at the driver level, everything is asynchronous. The synchronous Win32 APIs are just convenience wrappers.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • No really sure "driver level" makes any sense here. Can you cite a reference that says ever interaction with a driver in Windows is asynchronous? – Peter Ritchie Aug 02 '12 at 14:51
  • I was just pointing out that device drivers are asynchronous. So, e.g., if you do asynchronous network communications, there's no layer where an OS or kernel thread is blocking on the I/O so you can *pretend* it's asynchronous (like the `Stream` class does) - it's async *all* the way down. – Stephen Cleary Aug 02 '12 at 16:59
  • Reference: Windows Internals (5th ed), pg 515. You can, of course, write a driver that completes IRPs immediately, but you shouldn't write a driver that blocks the kernel thread until the IRP is complete. – Stephen Cleary Aug 02 '12 at 17:16
  • I think that assumes I/O, not every interaction with a driver is asynchronous. Even then, a driver is allowed not to return STATUS_PENDING on an overlapped request. But, I digress... – Peter Ritchie Aug 02 '12 at 20:19
2

Not 100% sure, but from the article it sounds like what's going on is that it allows windows messages (resize events, mouse clicks, etc) to be interleaved between the calls to FrobAsync. Roughly analogous to:

void FrobAll()
{
    for(int i = 0; i < 100; ++i)
    {
        FrobAsync(i); // somehow get a started task for doing a Frob(i) operation on this thread
        System.Windows.Forms.Application.DoEvents();
    }
}

or maybe more accurately:

void FrobAll()
{
    SynchronizationContext.Current.Post(QueueFrob, 0);
} 

void QueueFrob(Object state) {
    var i = (int)state;
    FrobAsync(i);
    if (i == 99) return;
    SynchronizationContext.Current.Post(QueueFrob, i+1);
}

Without the nasty call to DoEvents between each iteration. The reason that this occurs is because the call to await sends a Windows message that enqueues the FrobAsync call, allowing any windows messages that occurred between Frobbings to be executed before the next Frobbing begins.

Chris Shain
  • 50,833
  • 6
  • 93
  • 125
1

async/await is simply about asynchronous. From the caller side, it doesn't matter if multiple threads are being used or not--simply that it does something asynchronous. IO completion ports, for example, are asychronous but doesn't do any multi-threading (completion of the operation occurs on a background thread; but the "work" wasn't done on that thread).

From the await keyword, "multi-threaded" is meaningless. What the async operation does is an implementation detail.

In the sense of Eric's example, that method could have been implemented as follows:

return Task.Factory.StartNew(SomeMethod, 
                             TaskScheduler.FromCurrentSynchronizationContext());

Which really means queue a call to SomeMethod on the current thread when it's done all the currently queued work. That's asynchronous to the caller of FrobAsync in that FrobAsync returns likely before SomeMethod is executed.

Now, FrobAsync could have been implemented to use multithreading, in which case it could have been written like this:

return Task.Factory.StartNew(SomeMethod);

which would, if the default TaskScheduler was not changed, use the thread pool. But, from the caller's perspective, nothing has changed--you still await the method.

From the perspective of multi-threading, this is what you should be looking at. Using Task.Start, Task.Run, or Task.Factory.StartNew.

Peter Ritchie
  • 35,463
  • 9
  • 80
  • 98
  • I think I got it. The pattern doesn't care about threading at all. Literally. It knows how to suspend an 'async' method when it comes across a non-completed Task that is awaited, and it knows how to resume that method when the awaited Task completes. If the Task gets its work done by starting another thread, fine, that pattern doesn't know/care. If it gets its work done by calling some windows API that asynchronously gets the work done, fine, the pattern doesn't know/care. If it gets its work done by exporting it to some external system, fine, the pattern doesn't know or care. – Michael Ray Lovett Aug 02 '12 at 15:16
  • And as to my original question about whether asyonchrony on one thread was merely academic; apparently it's not. Apparently there's a fair number of API's that can be awaited on and who do effectively get their work done asynchronously, but do so without starting a separate thread. – Michael Ray Lovett Aug 02 '12 at 15:17
  • Correct, if you're moving towards WinRT, then there are many new Async methods that has synchronous-only counterparts in Win32 (or .NET) but are now invoked on a background thread because the original synchronous method could take more than 50 ms. .NET 4.5 should have similar new methods. – Peter Ritchie Aug 02 '12 at 15:20
  • So I could definitely write an application that makes use of "async/await" and never have to start up a separate thread myself. BUt if I'm going to offload my own work (in which case there certainly is no pre-written API to do my custom work asynchronously) them I'm going to have to figure out how to get that work done asynchronously. This most likely will involve Task.Run, etc. – Michael Ray Lovett Aug 02 '12 at 15:20