Task.Factory.StartNew won't wait for task completion?

12,203

Solution 1

The problem lays in the fact that Task.Factory.StartNew isn't "task aware". Meaning, the return type from your method call to StartNew is actually a Task<Task>. This means that you're only waiting on the outter task to complete, not the inner one.

A simple solution to that would be to use the TaskExtensions.Unwrap() method:

private static void Main(string[] args)
{
    var taskList = new List<Task>();
    taskList.Add(Task.Factory.StartNew(() => new Program().Foo()).Unwrap());

    Task.WaitAll(taskList.ToArray());
}

Task.Run does work because it is "task aware". It has an overload taking a Func<Task>, which internally calls Unwrap for you, returning only the inner task.

Solution 2

Task.Factory.StartNew doesn't await the completion of the inner task by default. If you create a variable with the return value, you will get this:

Task<Task> task = Task.Factory.StartNew(() => new Program().Foo());

This means that the delegate returns as soon as the inner task hits the await statement. You need to call UnWrap method to force the async execution:

Task task = Task.Factory.StartNew(() => new Program().Foo()).Unwrap();
taskList.Add(task);

Blog post you posted also explains this difference between synchronous and asynchronous delegates.

Solution 3

Your code waits for the end of new Program().Foo() with the statement Task.WaitAll(taskList.ToArray());

However, Foo doesn't wait for the end of client.SendAsync(requestMessage) before returning, since it has an await. The await will let the method return the Task object and allow the thread to come back on the next line.

The "problem" is that since you return on your await, the task that calls the Foo method is complete is complete at that time, even before you hit the output on the command line (The Task returned by the Foo method however, is not complete).

You basically have 3 "timelines" (I won't say thread because I'm not sure the HttpClient actually starts a new thread, but if someone can confirm this, I'll edit the post.) and 2 tasks:

  • your Main thread
  • the task created with StartNew
  • The the SendAsync task.

Your Main thread is waiting for the end of the Task created by StartNew, but that Task does not wait for the end of the SendAsync call, and therefore doesn't wait the command line output to consider itself as completed. Since only that one is considered in the WaitAll and the SendAsync isn't, it is normal that it returns that early.

To have the behaviour you want, you should have something like

taskList.Add(Task.Factory.StartNew(() => new Program().Foo().Wait()));

but then again, starting a new Task just to wait for another one wouldn't make much sense, so you'd better go with taskList.Add(new Program().Foo());

Share:
12,203
derekhh
Author by

derekhh

Currently I am a software engineer at Snap Inc. Previously I've been working at Microsoft Bing for almost four years. During my years at Microsoft I was the primary back-end algorithm developer or tech lead for many features related to natural language processing and machine learning. I've designed and implemented many core algorithms that powered Bing's conversational experience, question answering and entity carousel. Prior to joining Microsoft, I've obtained my Ph.D. from the Hong Kong University of Science and Technology. My research was primarily on the theme of sensor-based human activity recognition. Throughout my PhD years, I've published around 20 papers in top conferences and journals. I was also a winner of the Microsoft Research Fellowship in the year 2009. I also enjoy competitive programming a lot. I was a regular contestant in programming contests like acm/icpc, Google Code Jam and TopCoder Open. I've also won awards and top prizes from these competitions. Google Scholar Page: https://scholar.google.com/citations?user=Ks81aO0AAAAJ&amp;hl=en Specialties: machine learning, data mining, algorithms, programming

Updated on June 05, 2022

Comments

  • derekhh
    derekhh almost 2 years

    I've found that the following code won't actually wait for the task client.SendAsync() if I use the implementation:

    taskList.Add(Task.Factory.StartNew(() => new Program().Foo()));
    

    If I change it from Task.Factory.StartNew() to just new Program().Foo() or Task.Run(() => new Program.Foo() it will correctly output some information. What are the differences between the two?

    internal class Program
    {
        private async Task Foo()
        {
            while (true)
            {
                var client = new HttpClient();
                var requestMessage = new HttpRequestMessage(HttpMethod.Head, "http://www.google.com");
                HttpResponseMessage response = await client.SendAsync(requestMessage);
                Console.WriteLine(response.RequestMessage.RequestUri.ToString());
            }
        }
    
        private static void Main(string[] args)
        {
            var taskList = new List<Task>();
    
            // This won't output anything.
            taskList.Add(Task.Factory.StartNew(() => new Program().Foo()));
    
            // This will.
            taskList.Add(Task.Run(() => new Program().Foo()));
    
            // So does this.
            taskList.Add(new Program().Foo());
    
            Task.WaitAll(taskList.ToArray());
        }
    }
    

    Based on this MSDN article, it seems Task.Run(someAction); is equivalent to Task.Factory.StartNew(someAction, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

    But even when I change the code to this, it won't output anything. Why?

    internal class Program
    {
        private async Task Foo()
        {
            while (true)
            {
                var client = new HttpClient();
                var requestMessage = new HttpRequestMessage(HttpMethod.Head, "http://www.google.com");
                HttpResponseMessage response = await client.SendAsync(requestMessage);
                Console.WriteLine(response.RequestMessage.RequestUri.ToString());
            }
        }
    
        private static void Main(string[] args)
        {
            var taskList = new List<Task>();
            taskList.Add(Task.Factory.StartNew(() => new Program().Foo(), CancellationToken.None,
                TaskCreationOptions.DenyChildAttach, TaskScheduler.Default));
            //taskList.Add(Task.Run(() => new Program().Foo()));
            //taskList.Add(new Program().Foo());
            Task.WaitAll(taskList.ToArray());
        }
    }
    
    • Arghya C
      Arghya C over 8 years
      Answer of the question in subject line - No, unless you ask for.
    • Larry
      Larry over 8 years
      Have a look to the async/await pattern. This will broad your horizon around this issue.
    • Stephen Cleary
      Stephen Cleary over 8 years
      @derekhh: Read your whole reference. The part you're quoting is prefixed with When you pass an Action to Task.Run, which your code is not doing. Further down the blog post he explains how Task.Run behaves differently with asynchronous delegates.
  • vighnesh153
    vighnesh153 over 4 years
    for a single task: var task = Task.Factory.StartNew(something); task.Wait();