2

I understand that the Task-based Asynchronous Pattern (TAP) is now the preferred way to write async code but I was reading up on previous patterns to see how async code used to be written.

When writing BeginXXX methods, was there any suggested way to do it or was it totally left up to the implementor of the method?

I found some cases where tasks are used but my understanding is that Tasks were introduced with the TPL in .NET Framework 4. So before that, did BeginXXX calls call BeginInvoke on some delegate (which I guess kicks off a ThreadPool thread?) or did classes just use ThreadPool directly, or something else, or a mix, etc.?

1
  • 1
    There were several rounds of cleanup to ensure all async APIs are task based (old GitHub PRs like github.com/dotnet/corefx/pull/16502 keep the records), so what you are looking at now is the final result. While this is a good question and investigation to go, don't dig too deep into such, as there isn't likely to be extensive documentation.
    – Lex Li
    Commented Jul 9 at 20:27

1 Answer 1

2

did BeginXXX calls call BeginInvoke on some delegate (which I guess kicks off a ThreadPool thread?) or did classes just use ThreadPool directly, or something else, or a mix, etc.?

Generally neither. That wouldn't be the correct way to do Async, and isn't generally used in the new Task APIs either. It's an inefficient waste of a threadpool thread with unnecessary blocking. Yes, there probably are some APIs that do this, but not many and it was never a favoured approach unless there was no other way to enable async in that API.

What BeginXXX would normally do is send off whatever request is being done all the way down to whichever device driver it needs to, with the instruction to call the callback when it's done. Then it returns immediately.

For example, with FileStream.BeginRead:

  • It takes the buffer and passes it to the Windows File API. This uses something called Overlapped IO, which takes a callback function pointer. This is what will essentially inform your app that the read is completed.
  • That then transitions to Kernel Mode, and passes the buffer and callback on to the kernel, which then tells the driver to execute the read.
  • The driver (assuming it can't find the data in RAM cache and complete synchronously) tells the disk to execute the read. The disk will do this using Direct Memory Access (DMA), entirely separate from the CPU, so it immediately confirms that the request is received.
  • The driver tells this to the kernel, and in turn the user-mode WinAPI and .NET code, that the read is requested. BeginRead completes and returns to the caller.

Note that at this stage the threadpool is not involved.

  • The disk completes the read, and interrupts the CPU.
  • The kernel and driver mark the Overlapped IO as completed.
  • The callback in usermode marks the IAsyncResult as completed. The callback you passed to BeginRead is called.
    • Technically speaking, this actually goes via an IO Completion Port, which is a special type of wait handle that the threadpool waits on. This is only briefly woken up, and just marks the IAsyncResult as completed. It then posts your callback to the main threadpool queue to call.
    • The threadpool didn't need to be involved, it could just as well have let you manage it all yourself by only waiting on the Overlapped directly in EndRead, but this way it means you don't need to set up the callback and yourself, and it's also more efficient from a thread management perspective.

It's up to you how you want to get the result of that read. You get an IAsyncResult but there are a number of ways to handle that. If you just call EndRead directly then you will block your thread waiting on the Overlapped IO. You could also poll it periodically.

The recommended approach historically was to pass a delegate callback which directly calls EndRead to pick up the result and continue your code. A more recent development is to use TaskFactory.FromAsync, which sets all this up for you automatically, and include optimistic checking for immediate synchronous completion.

The point being: you don't need to use tasks to use the Begin End APM model, and you normally wouldn't use Task.Run to "pretend" you were doing async.


I've outlined how FileStream does async, but most async IO functions work on the same principle: they hand off the callback all the way to the kernel and device, and the kernel interrupt handler picks up the callback when it's done. As @StephenCleary says: There Is No Thread. Nothing is waiting on your callback, the devices just go off and do their stuff.

2
  • Thanks, this is a great explanation. One question though. Is what you described only relevant for IO operations? For example, if some library had a long running CPU-bound calculation, and offered a BeginXXX method, I guess in that case a thread does somehow need to be spawned? (Though I am not sure if such an example exists in any .Net API).
    – Flack
    Commented Jul 11 at 18:18
  • It would be very strange if there was such a thing and I'm not aware of any that do so. In that case yes you would need something like Task.Factory.StartNew with TaskCreationOptions.LongRunning or similar. It doesn't really make sense to do that: for example on ASP.NET it woldn't work at all as you are limited to one thread per request. So it makes more sense to let you manage the threading yourself. Commented Jul 11 at 20:51

Not the answer you're looking for? Browse other questions tagged or ask your own question.