There are three semi-independent systems at work here: the thread pool (with work-stealing queues), the ASP.NET request context, and async/await.
The thread pool works as you describe: each thread has its own queue but can steal from other threads' queues if necessary. But this actually has little to do with how async/await works on ASP.NET. For the most part, you can completely ignore how work stealing queues work because the logical abstraction is of a single thread pool with a single queue. The work stealing queues are just an optimization.
The ASP.NET request context manages things like HttpContext.Current, security, and culture. It is not tied to a specific thread, but only one thread is allowed within a context at a time. This pattern is true for old-style asynchronous requests as well as the new-style async requests. Note that a request is bound to a thread from beginning to end only for synchronous requests; this is not true for asynchronous requests (and never has been). The ASP.NET request context is implemented as a synchronization context - specifically, an instance of AspNetSynchronizationContext.
When your code awaits an incomplete Task, by default await will capture the current context (which is SynchronizationContext.Current unless it is null, in which case it is the current TaskScheduler). When the Task completes, then the async method is continued within that context. I describe this behavior in more detail on my blog. You can think of async/await as "thread agnostic"; that is, they don't necessarily resume on a different thread, nor do they necessarily resume on the same thread. They leave all threading decisions up to the captured context.
One other side note is that there are two different types of Tasks, Promise Tasks and Delegate Tasks (as I describe on my blog). Only Delegate Tasks actually have code to run and are queued to the thread pool at all. So, when the await decides to suspend its method, it has no code to run and nothing is queued at that time; rather, it sets up a callback (continuation) that will queue the rest of the method in the future.
When the awaited task completes, that callback/continuation is run, which queues the remainder of the async method to that captured context. In theory, this could queue it to the thread pool, but in reality there's a shortcut that is almost always taken: The thread completing the task is usually a thread pool thread itself, so it just enters the request context directly and then resumes executing the async method without actually having to queue it anywhere.
So in the vast majority of cases, work-stealing queues don't come into play at all. It really only happens when the thread pool is overloaded with work.
But do note that it is entirely possible (and common) to have an async handler start on one thread and continue on another thread. This is usually not a problem because the request context is preserved, but thread-affine constructs like thread-local variables will not work correctly.
Task#ConfigureAwait(false)I think it's a given that async/await does change its synchronization context. ASP.NET is indeed a special case as far as I've read but I don't know how it is handled exactly. - Jeroen Vannevel