Waiting for an asynchronous pattern and working with concise streams

I'm trying to understand the basic mechanism of the async / await pattern, and I thought I got it after reading the next article Work-Steal in .NET 4.0 by Jennifer Marsman. I understand that

1 - there is a global queue for the thread pool as well as local queues for each thread in the thread pool

2 - The request arrives, is in the global queue, and then thread1 (T1) from the thread pool grabs the request.

3 - This request is an async \ await method. After pressing the wait keyword, a bookmark (callback) is created, wrapped in a task (assuming the task has not completed), and that task is placed on the local queue T1. T1 goes back to the pool

4 - When the task completes, if T1 is not busy, T1 will process the request. But if T1 is busy, another thread (call it T2) can actually steal this task from the local queue T1

This is where my question comes in. How is it prohibited? Everything I read assumes that async \ await does not change the threading context. See the link for an MSDN explanation async \ await This also makes sense since in an MVC application the request is tied to a thread. This means that if a request comes to an asynchronous action method, I expect both the initial and continued task to be executed by the same thread pool thread. How does thread stealing work without getting in the way? Appreciate any understanding.

+3


source to share


2 answers


There are three semi-independent systems running here: a thread pool (with job search queues), an ASP.NET request context, and async

/ await

.

The thread pool works as you describe: each thread has its own queue, but can steal from the queues of other threads if necessary. But it really has little to do with how async

/ await

works in ASP.NET. For the most part, you can completely ignore how queue stealing operations work because the logical abstraction consists of a single thread pool with a single queue. Job theft queues are just optimization.

The ASP.NET request context controls things like HttpContext.Current

security and culture. It is not tied to a specific thread, but only one thread is allowed per context at a time. This pattern is valid for old-style asynchronous requests as well as async

new-style requests . Note that the request is thread-bound from start to finish for synchronous requests only; this is not true for asynchronous requests (and never was). The ASP.NET request context is implemented as a synchronization context - specifically, an instance AspNetSynchronizationContext

.

If your code is await

incomplete Task

, the default await

will capture the current context (which SynchronizationContext.Current

, if it isn't null

, in which case it's the current one TaskScheduler

). When it completes Task

, then the method async

continues in this context. I describe this behavior in more detail on my blog . You can think of async

/ await

as "thread agnostic"; that is, they do not necessarily resume on another thread, nor do they necessarily resume on the same thread. They leave all streaming decisions up to the captured context.



Another side of the note is that there are two different types of Task

s, promise tasks and delegation tasks (as I describe in my blog ). Only task delegation really has the code to run and is queued to the thread pool at all. So when it 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 stop the rest of the method in the future.

When the awaited task completes, this callback / continuation is triggered, which puts the rest of the method async

in this captured context. In theory this could queue up the thread pool, but in reality there is a shortcut that almost always gets executed: the thread performing the task is usually the thread of the thread, so it just goes directly into the request context and then resumes execution async

without having to queue.

Thus, in the vast majority of cases, work queues do not come into play at all. This only happens when the thread pool is overloaded with work.

But note that it is perfectly possible (and usually) to have a handler async

on one thread and continue on another thread. This is usually not a problem because the request context is preserved, but thread-affine constructs such as thread-local variables will not work correctly.

+5


source


The way this is handled is that each thread has a global SyncronisationContext objectassociated with it. This object is responsible for handling how the task resumes as different situations have different requirements. For example, if your GUI is running for the suspend to be transparent, the task must be resumed on a single thread. However, if you are on a thread that does not have a specific context, then the default SyncronisationContext is used, which resumes in the threadpool. So the whole threadpool uses one SyncronisationContext with any threads that didn't have one. But GUI threads and MVC action threads set up special SyncronisationContexts for each thread to ensure that it resumes on the same thread. It is also possible to configure the task to resume in another SyncronisationContext using ConfigureAwait.



TL; DR Job Steal cannot cross between SyncronisationContext, but a single SyncronisationContext can contain multiple threads to allow job theft.

+2


source







All Articles