September 8, 2024

Thread.Delay() -- a second look at async/await

I decided to cut out the middle man, and just call Task.Delay() directly from the main thread. No second thread to manage the Delay. It all compiles just fine. Note the "async" marking on the wait() call as required. The problem is that it doesn't work! We get no delay -- all our messages come out all at once.

Now I could sweep this under the rug and not show you my mistakes, but I figure that you know as well as I do that things rarely work out right the first time in this software business. I'll risk the chance that you consider be a dummy and admire my honestly. Meanwhile, I'll go read about this await business and come back when I can explain what is going on.

using System;
using System.Threading.Tasks;

namespace Example
{
    public class Wally
    {
        public static int Delay { private get; set; }

        public static void old_wait ()
        {
            Thread.Sleep ( Delay );
        }

		public static async void wait ()
        {
            await Task.Delay ( Delay );
        }

        public static void chat ( string msg, int repeat )
        {
            for ( int i=0; i<repeat; i++ ) {
                Console.WriteLine ( msg );
                Wally.wait ();
            }
        }
    }
}

namespace HogHeaven
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Example.Wally.Delay = 1000;
            Example.Wally.chat ( "Kilroy is here !!", 4 );
            Console.WriteLine ( "All done" );
        }
    }
}
// THE END

Why doesn't this work?

Because async/await are more complicated than you think. It is no accident that good books devote at least one, maybe two chapters to the topic, and even then warn you that they have only begun to explain the topic. Usually a third chapter on Tasks will also need to be digested. Saying this is complex is not a cop-out -- it truly is. Concurrent programming (where several things are going on simultaneously) has always been recognized as a difficult programming issue.

I expect my understanding to grow in stages, as I peel the layers of this onion. Here is what I know so far. The await statement actually splits the enclosing async method into two halves, let's call them the first and the last half. The first half runs as you might think, but at the await statement, execution suddently splits into two paths! Path 1 goes immediately to the end of the enclosing async method and returns. Path 2 performs the call after the await and then arranges to run the last half of the enclosing method when the call finishes. This last half is packaged up as a "continuation" and will be run by some thread up to the end of the async method, then is finished.

How about that. So looking at the above code with this new insight, we can understand why it doesn't work. Every time we call wait, it returns immediately (just what we see). We end up with four "last halves" of the wait method staged to run when the delay finishes. These "last halves" will do nothing, and there is a good chance the compiler will recognize that and just optimize them away -- but who knows.

We will rearrange and "fix" this in the next section.


Feedback? Questions? Drop me a line!

Tom's Computer Info / tom@mmto.org