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.?
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
:
BeginRead
completes and returns to the caller.Note that at this stage the threadpool is not involved.
IAsyncResult
as completed. The callback you passed to BeginRead
is called.
IAsyncResult
as completed. It then posts your callback to the main threadpool queue to call.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.