CodeMonkey
CodeMonkey

Reputation: 12424

How is a task with await executed with no additional threads

This is a follow up question to this question:

If async-await doesn't create any additional threads, then how does it make applications responsive?

and to this blog post:

http://blog.stephencleary.com/2013/11/there-is-no-thread.html

Regarding the accepted answer of the linked question.. He describes steps of what happens when await is called.

He writes there on step 3 and 4 that when that happens, the current thread returns to the calling context which makes the message loop available to receiving additional messages which makes the application responsive. And then in step 5 he writes that the task which we called await on is now complete and the rest of the original method continues after receiving a new message to continue the rest of the method.

What I didn't understand there is how exactly that task was executed to begin with if there wasn't a new thread, and the original thread is busy with the calling context?

So I turned to the blog post, which describes how the entire operation is actually done in much lower levels (which honestly I don't really know how they work) and then there is simply a notification that it was done... But I don't understand why suddenly in the case of the task, you can rely on other hardware for your calculations instead of the CPU which somehow makes it possible not to create a new thread.. And what if this task is some complicated calculations, then won't we need the CPU? And then we return to what I was writing that the current thread is already busy with the calling context...

Upvotes: 1

Views: 393

Answers (3)

Dmytro Mukalov
Dmytro Mukalov

Reputation: 1994

But I don't understand why suddenly in the case of the task, you can rely on other hardware for your calculations instead of the CPU which somehow makes it possible not to create a new thread.

Because CPU is not only possible source of parallelism in a hardware devices set.

The threads are logically related to CPU, so for any CPU-bound workload we use threads either explicitly by creating them or using higher-level mechanisms like thread pool or Task.Run or implicitly - every application starts inside some default thread anyway.

But there is another sort of operations - input/output operations which imply using other than CPU<->RAM hardware devices such as disks, network adapters, keyboard, various peripheral devices, etc. Such devices deal with data which comes in and comes out asynchronously - nobody knows when you next time press a key or new data arrives from a network. To deal with asynchrony, such hardware devices are able to transmit the data without a CPU involvement (in simple words a device is provided with some addresses in RAM where data resides and then it can do the transmission by itself). It's very simplistic picture of what happens, but that's something you can think most of the asynchronous flows end with. As you can see CPU isn't required there, so no need to create a new thread. As for the inbound data the mechanism is pretty similar with only difference that once data is arrived it's placed into specific RAM area to be available for further consumption. When a device completes data transition (inbound or outbound), it raises specific signal called interruption to notify CPU about operation completion and CPU reacts on the interruption with triggering of specific code execution which typically resides in the hardware device driver - in that way the driver can send the notification to the higher levels. The interruptions may come from devices asynchronously and CPU is obligated to suspend any current execution it's doing at the moment and switch to an interruption handler. When a device driver is executing the interruption handler it sends the notification about I/O completion to the higher levels of OS stack and finally this notification hits the application which initiated the I/O operation. How it's done is mostly dependent on OS which the application is running on. For Windows there is specific mechanism called I/O Completion Ports which implies using some thread pool to handle I/O completion notifications. These notifications finally come from CLR to the application and trigger execution of continuations which eventually can run on separate thread which is either thread from I/O thread pool or any other thread, depending on specific awaiter implementation.

As for the general idea of the article you're referring, it can be rephrased with: there is no thread behind async/await unless you explicitly create it, because the await/async is only advanced notifications framework per se, which can be extended to work with any asynchronous mechanism underneath.

Upvotes: 3

Stephen Cleary
Stephen Cleary

Reputation: 456507

What I didn't understand there is how exactly that task was executed to begin with if there wasn't a new thread, and the original thread is busy with the calling context?

It's important to note that there are two types of tasks: what I call Delegate Tasks and Promise Tasks. Delegate Tasks are the types of tasks used in parallel processing - the original Task Parallel Library style of usage, where the task represents some amount of code to be executed.

Promise Tasks, on the other hand, only provide a notification that something has completed (along with the result/exception of that operation). Promise Tasks do not execute code; they are an object representation of a callback. async always uses Promise Tasks.

So, when an async method returns a Task, that task is a Promise Task. It doesn't "execute", but it can "complete".

Upvotes: 2

Enigmativity
Enigmativity

Reputation: 117064

I think you need to understand the level at which the compiler goes to to make async/await code work.

Take this method:

public async Task<int> GetValue()
{
    await Task.Delay(TimeSpan.FromSeconds(1.0));
    return 42;
}

When this is compiled you get this:

[AsyncStateMachine(typeof(<GetValue>d__1))]
public Task<int> GetValue()
{
    <GetValue>d__1 stateMachine = default(<GetValue>d__1);
    stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
    stateMachine.<>1__state = -1;
    AsyncTaskMethodBuilder<int> <>t__builder = stateMachine.<>t__builder;
    <>t__builder.Start(ref stateMachine);
    return stateMachine.<>t__builder.Task;
}

[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct <GetValue>d__1 : IAsyncStateMachine
{
    public int <>1__state;

    public AsyncTaskMethodBuilder<int> <>t__builder;

    private TaskAwaiter <>u__1;

    private void MoveNext()
    {
        int num = <>1__state;
        int result;
        try
        {
            TaskAwaiter awaiter;
            if (num != 0)
            {
                awaiter = Task.Delay(TimeSpan.FromSeconds(1.0)).GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    num = (<>1__state = 0);
                    <>u__1 = awaiter;
                    <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
                    return;
                }
            }
            else
            {
                awaiter = <>u__1;
                <>u__1 = default(TaskAwaiter);
                num = (<>1__state = -1);
            }
            awaiter.GetResult();
            result = 42;
        }
        catch (Exception exception)
        {
            <>1__state = -2;
            <>t__builder.SetException(exception);
            return;
        }
        <>1__state = -2;
        <>t__builder.SetResult(result);
    }

    void IAsyncStateMachine.MoveNext()
    {
        //ILSpy generated this explicit interface implementation from .override directive in MoveNext
        this.MoveNext();
    }

    [DebuggerHidden]
    private void SetStateMachine(IAsyncStateMachine stateMachine)
    {
        <>t__builder.SetStateMachine(stateMachine);
    }

    void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
    {
        //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
        this.SetStateMachine(stateMachine);
    }
}

It's the IAsyncStateMachine.MoveNext() that allows the allows the calling code to drop out when it hits the await. Note the return; in the if (!awaiter.IsCompleted).

It's just a state-machine that does all of the magic.

Upvotes: -1

Related Questions