C# async/await: Pedantry vs. the Debugger

asyncasynchronous-programmingmultithreadingnet

I'm playing around with async and await, and they seem pretty intuitive, but some of the things I'm reading about these keywords doesn't make sense to me. In fact, some of it seems to me to be flat-out wrong, or at least misleading, even though it's coming straight from Microsoft. Consider the code below:

namespace AsyncExample2
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private async void button1_Click(object sender, EventArgs e)
        {   
            var t=new Task<int>(longRunningWork);
            t.Start(); 
            int r= await t;
            textBox1.Text += (r.ToString());
        }

        int longRunningWork()
        {
            Thread.Sleep(15000); 
            return (new Random()).Next(10);
        }
    }
}

This is from a WinForms example application I created with a GUI consisting of a single button and a single textbox. When you press the button, 15 seconds later, a random digit is appended to the text in the textbox. During this 15 second period, the GUI remains interactive, e.g. the form can be maximized/minimized/moved. The button can even be clicked again.

The most basic way that this code seems to contradict what Microsoft says about await / async is that, according to them, my call to t.Start() should not be necessary. Consider the very first statement of the article at https://msdn.microsoft.com/en-us/library/hh696703.aspx: "[i]n an async method, tasks are started when they’re created." This does not seem to be the case in this particular async method (button1_Click), though I have seen it happen in other contexts.

Second, I've read a lot of what I would describe as aggressive attempts to differentiate between the old thread-based model of parallelism and async / await, and, like the "All New!" labels often applied to product packaging by corporate marketing departments, these attempts at differentiation seem to contain some unjustified bluster.

Consider the statement that "the async and await keywords don't cause additional threads to be created" (from https://msdn.microsoft.com/en-us/library/hh191443.aspx). This is not my observation; if I inspect Thread.CurrentThread.ManagedThreadId, I can see that longRunningWork() is not running on the GUI thread, though button1_Click certainly is. Furthermore, if I click the button rapidly in succession, and inspect Thread.CurrentThread.ManagedThreadId at the call to Thread.Sleep(), I see various, different managed thread IDs. And if I try to add anything to longRunningWork() that affects the GUI (say, sets textBox1.Text), I get the typical "illegal cross-thread call" error.

Can someone help me reconcile these seeming inconsistencies? My hunch is that the first inconsistency I highlight is a relatively minor example of sloppiness (though I'd really like to know when I need to call Start() manually).

The second inconsistency, I suspect, is just pedantry. People who know a little bit about async / await like to correct noobs who can't talk about these keywords without thinking in terms of threads. I get it; it's a different model of parallelism with a wholly different way of thinking. At the same time, though, it's a definite oversell to say that "the async and await keywords don't cause additional threads to be created." The debugger implies otherwise.

I suppose that technically it's possible that the threads I'm seeing are being grabbed from some sort of pool that exists throughout the program's execution. If that's the case, then, technically, nothing is getting created by async / await. This seems pretty trivial to me, and I'm not aware that WinForms programs have an ASP.NET-style thread pool.

Best Answer

You could rewrite your code this way in a pure async/await paradigm.

    private async void button1_Click(object sender, EventArgs e)
    {
        int r = await longRunningWork();
        textBox1.Text += (r.ToString());
    }

    private async Task<int> longRunningWork()
    {
        await Task.Delay(15000);
        return (new Random()).Next(10);
    }

The fact a thread is created is up to the CLR to decide. What the await does in the button1_Click method is that it returns the control to the calling thread of button1_Click (UI thread) which can do other work. The compiler introduces automatic code everytime it sees await to add some goto at the end of the awaitable method whenever it is completed to continue the execution where it left.

My guess here why I think your code spawn a new thread is because you call Thread.Sleep(15000); which is a synchronous call, hence if the CLR wants to give control back to the UI thread, it knows it has to spawn a new thread.

Even your original longRunningWork method is synchronous so it definitely needs a new thread.