How do I create a Task that uses await inside the body that behaves the same as the synchronous version when Wait is called?

11,466

Solution 1

It seems like this isn't possible! See alexm's answer here:

Tasks returned by async methods are always hot i.e. they are created in Running state.

:-(

I've worked around this by making my agent queue Func<Task>s instead, and the overload that receives a task simply queues () => task. Then; when de-queing a task, I check if it's not running, and if so, start it:

var currentTask = currentTaskFunction();
if (currentTask.Status == TaskStatus.Created)
    currentTask.Start();

It seems a little clunky to have to do this (if this simple workaround works; why the original restriction on async methods always being created hot?), but it seems to work for me :-)

Solution 2

You could write this as:

public static async Task Wait2()
{
    Console.WriteLine("Waiting...");
    await Task.Delay(10000);
    Console.WriteLine("Done!");
}

In general, it's rarely a good idea to ever use new Task or new Task<T>. If you must launch a task using the ThreadPool instead of using the async/await language support to compose one, you should use Task.Run to start the task. This will schedule the task to run (which is important, tasks should always be "hot" by conventions).

Note that doing this will make it so you don't have to call Task.Start, as well.

Solution 3

To help you understand this realize that async / await essentially does not create a new thread but rather it schedules that portion of code to be ran at an available point in time.

When you create the new Task(async () => ...) you have a task that run an async method. When that inner async method hits an await the 'new Task' is considered complete because the rest of it has been scheduled. To help you understand better place some code (a lot if wanted) in the 'new Task' before the await command. It will all execute before the application terminates and once await is reached that task will believe it has completed. It then returns and exits the application.

The best way to avoid this is to not place any task or async methods inside of your task.

Remove the async keyword and the await keyword from the method and it will work as expected.

This is the same as creating a callback if you're familiar with that.

void MethodAsync(Action callback)
{
     //...some code
     callback?.Invoke();
}

//using this looks like this.
MethodAsync(() => { /*code to run when complete */});

//This is the same as

Task MethodAsync()
{
     //... some code here
}

//using it

await MethodAsync();
/*code to run when complete */

The thing to understand is that you're creating a new task within a task basically. So the inner 'callback' is being created at the await keyword.

You're code looks like this..

void MethodAsync(Action callback)
{
     //some code to run
     callback?.Invoke();  // <- this is the await keyword
     //more code to run.. which happens after we run whoever is
     //waiting on callback
}

There's code missing obviously. If this doesn't make sense please feel free to contact me and I'll assist. async / await (meant to make things simpler) is a beast to wrap your head around at first. Afterward you get it then it'll probably be your favorite thing in c# since linq. :P

Share:
11,466
Danny Tuppeny
Author by

Danny Tuppeny

Updated on June 26, 2022

Comments

  • Danny Tuppeny
    Danny Tuppeny almost 2 years

    I have some code that creates a task that does some slow work like this:

    public static Task wait1()
    {
        return new Task(() =>
        {
            Console.WriteLine("Waiting...");
            Thread.Sleep(10000);
            Console.WriteLine("Done!");
        });
    }
    

    In the real implementation, the Thread.Sleep will actually be a web service call. I would like to change the body of the method can use await (so it does not consume a thread during the network access/sleep). My first attempt (based on shotgun-debugging the compile errors) was this:

    public static Task wait2()
    {
        return new Task(async () =>
        {
            Console.WriteLine("Waiting...");
            await Task.Delay(10000);
            Console.WriteLine("Done!");
        });
    }
    

    However; this task doesn't seem to behave the same as the first one, because when I call .Wait() on it; it returns immediately.

    Below is a full sample (console app) showing the differences (the app will end immediately when the second task starts).

    What do I need to do so that I can call Start and Wait on a Task which happens to have code using await inside it? The tasks are queued and executed later by an agent, so it's vital that the task is not auto-started.

    class Program
    {
        static void Main(string[] args)
        {
            var w1 = wait1();
            w1.Start();
            w1.Wait(); // This waits 110 seconds
    
            var w2 = wait2();
            w2.Start();
            w2.Wait(); // This returns immediately
        }
    
        public static Task wait1()
        {
            return new Task(() =>
            {
                Console.WriteLine("Waiting...");
                Thread.Sleep(10000);
                Console.WriteLine("Done!");
            });
        }
    
        public static Task wait2()
        {
            return new Task(async () =>
            {
                Console.WriteLine("Waiting...");
                await Task.Delay(10000);
                Console.WriteLine("Done!");
            });
        }
    }