Home > Software design >  Who runs the awaitable in an Async Await context
Who runs the awaitable in an Async Await context

Time:12-30

I've been reading, and I noticed that Async/Await doesn't necessarily create a new thread, when it runs on I/O or any other hardware components.

But what happens when the operation is CPU Bound?

    public partial class Form1 : Form
    {
        private static List<string> _logs = new List<string>();

        public Form1()
        {
            Print("Form constructor");
            InitializeComponent();
        }

        private async void button1_Click(object sender, EventArgs e)
        {
            Print("Caller");
            await SomeLongOperation();
        }

        private async Task SomeLongOperation()
        {
            Print("Called");
            await Task.Delay(10000);
            Print("Called after task");
        }

        public void Print(string txt)
        {
            _logs.Add($"I'm writing from thread {Thread.CurrentThread.ManagedThreadId}. And, {txt}");
        }
    }

Let's say I have this class, if the Main Thread calls the button1_Click method runs up to the awaitable, and then the rest of the method is queued on the message loop, and thus allowing the main thread continue to work, then who is running the awaitable, since the main thread is already working on the UI and waiting for the awaitable to complete.

CodePudding user response:

I think you're confusing "asynchronous" with "parallel".

  • Asynchronous means that the thread is freed up to do other things while it is waiting for something else. It's about how your code waits.
  • Parallel is when two parts of your code are running at the same time. This can only be done with multiple threads. It's about how your code runs.

Your example code is asynchronous, but not parallel. Everything is done on one thread. Task.Delay is not a CPU operation. There is nothing for the thread to do. There is no thread.

If you truly had some CPU-intensive operation to run, you can offload that to another thread with Task.Run. For example:

await Task.Run(() => DoSomethingIntensive());

That mixes the idea of asynchronous with parallel. DoSomethingIntensive() is run on a separate thread. That's parallel. But the main thread waits asynchronously, meaning that it is free to go and do other things (like respond to user input) while it waits for the other thread to do its thing.

CodePudding user response:

This question appears to be about two related but different concepts. The first is when asynchronous method calls are offloaded to threadpool threads. The second is about context and how continuations are run. First, let's discuss threadpool threads.

I've been reading, and I noticed that Async/Await doesn't necessarily create a new thread, when it runs on I/O or any other hardware components.

This is not just true of IO-bound code. This is true for any asynchronous method:

The async and await keywords don't cause additional threads to be created. Async methods don't require multithreading because an async method doesn't run on its own thread. The method runs on the current synchronization context and uses time on the thread only when the method is active. You can use Task.Run to move CPU-bound work to a background thread, but a background thread doesn't help with a process that's just waiting for results to become available.

Consider this example:

private static List<string> _logs = new List<string>();

private async void TrueAsyncBtn_Click(object sender, EventArgs e)
{
    await TrueAsync();
}

private async void CpuBoundBtn_Click(object sender, EventArgs e)
{
    await CpuBoundAsync();
}

private async Task TrueAsync()
{
    Print("Called TrueAsync()");
    await Task.Delay(10000);
    Print("Called after TrueAsync() task");
}

private Task CpuBoundAsync()
{
    Print("Called CpuBoundAsync()");
    //Thread.Sleep() blocks. Could be any long-running synchronous operation
    //like computing pi or fibonacci sequence
    Thread.Sleep(10000); 
    Print("Called after CpuBoundAsync() task");
    return Task.CompletedTask;
}

public void Print(string txt)
{
    _logs.Add($"I'm writing from thread {Thread.CurrentThread.ManagedThreadId}. And, {txt}");
}

This simple UI has two buttons, a button to do a truly asynchronous operation (using Task.Delay), and a CPU-bound asynchronous operation (Thread.Sleep). The truly asynchronous method does not block the UI thread, but the CPU-bound asynchronous method does. The compiler has no way to know what is CPU-bound and what isn't. So Thread.Sleep() runs on the UI thread and blocks.

You can queue CPU-bound work to be run on a threadpool thread using Task.Run(). Consider the following additional method:

private async void CpuBoundThreadPoolBtn_Click(object sender, EventArgs e)
{
    await CpuBoundThreadPoolAsync();
}

private async Task CpuBoundThreadPoolAsync()
{
    Print("Called CpuBoundThreadpoolAsync()");
    await Task.Run(() => Thread.Sleep(10000));
    Print("Called after CpuBoundThreadpoolAsync() task");
}

This new method does not block the UI thread. The reason is because the CPU-bound work (Thread.Sleep) has been queued to run on a threadpool thread. The UI is free to do other things, like process messages to keep the UI responsive.

Finally, let's address context. The context determines where the continuation of the method is run. The continuation is where the rest of your work happens after the await. Context is captured transparently and automatically. So if you await an awaitable on the UI thread, the UI context is automatically captured.

To understand why this is important, consider these two examples:

private async void WithContextBtn_Click(object sender, EventArgs e)
{
    await WithContextAsync();
}

private async void WithoutContextBtn_Click(object sender, EventArgs e)
{
    await WithoutContextAsync();
}

private async Task WithContextAsync()
{
    WithContextBtn.Enabled = false;
    await Task.Delay(5000);
    WithContextBtn.Enabled = true;
}

//Produces cross-thread exception. 
private async Task WithoutContextAsync()
{
    WithoutContextBtn.Enabled = false;
    await Task.Delay(5000).ConfigureAwait(false);
    WithoutContextBtn.Enabled = true;
}

These methods disable their respective buttons, queue some CPU-bound work to run on a threadpool thread, and re-enable the buttons after the work is complete. The WithContextAsync() method shows the default behavior of capturing contexts. The UI context is captured when the method is awaited. When the threadpool thread is done the work, the continuation is run on the UI thread. The button is enabled without issue.

The WithoutContextAsync() call demonstrates what would happen if you don't have the UI context. When the threadpool thread is done running, the continuation does not run on the UI thread. The ConfigureAwait(false) removed the UI context, so the continuation likely runs on another threadpool thread. Code may not modify the UI unless it is run on the UI thread. As a result, when the method tries to set the button state to enabled, you get a cross thread exception.

CodePudding user response:

The async and await keywords doesn't actually cause additional creation of threads. Async methods don't require multithreading because an async method doesn't run on its own thread. The method actually runs on the current synchronization context and uses time on the thread only when the method is active. Developers can use Task.Run to move CPU-bound work tasks to a background thread. having said that, a background thread doesn't help with a process that's just waiting for results to become available.

More about async/await and threading can be found here - https://docs.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-2.2#automatic-http-400-responses -

  • Related