19

While trying to figure out the new (maybe not so new now, but new to me, anyway) Task asynchronous programming in C#, I ran into a problem that took me a bit to figure out, and I'm not sure why.

I have fixed the problem, but I am still not sure why it was a problem to begin with. I just thought I'd share my experience in case anyone out there runs into the same situation.

If any gurus would like to inform me of the cause of the problem, that'd be wonderful and much appreciated. I always like knowing just why something doesn't work!

I made a test task, as follows:

Random rng = new Random((int)DateTime.UtcNow.Ticks);
int delay = rng.Next(1500, 15000);
Task<Task<object>> testTask = Task.Factory.StartNew<Task<object>>(
    async (obj) =>
        {
            DateTime startTime = DateTime.Now;
            Console.WriteLine("{0} - Starting test task with delay of {1}ms.", DateTime.Now.ToString("h:mm:ss.ffff"), (int)obj);
            await Task.Delay((int)obj);
            Console.WriteLine("{0} - Test task finished after {1}ms.", DateTime.Now.ToString("h:mm:ss.ffff"), (DateTime.Now - startTime).TotalMilliseconds);
            return obj;
        },
        delay
    );
Task<Task<object>>[] tasks = new Task<Task<object>>[] { testTask };

Task.WaitAll(tasks);
Console.WriteLine("{0} - Finished waiting.", DateTime.Now.ToString("h:mm:ss.ffff"));

// make console stay open till user presses enter
Console.ReadLine();

And then I ran the application to see what it spat out. Here is some sample output:

6:06:15.5661 - Starting test task with delay of 3053ms.
6:06:15.5662 - Finished waiting.
6:06:18.5743 - Test task finished after 3063.235ms.

As you can see, the Task.WaitAll(tasks); statement didn't do much. It waited a grand total of 1 millisecond before continuing execution.

I have answered my own "question" below - but as I said above - if anyone more knowledgeable than I would care to explain why this doesn't work, please do! (I think it might have something to do with the execution 'stepping-out' of the method once it reaches an await operator - then stepping back in once the awaiting is done... But I am probably wrong)

i3arnon
  • 113,022
  • 33
  • 324
  • 344
cjk84
  • 347
  • 1
  • 2
  • 10
  • Why are you doing `new Random((int)DateTime.UtcNow.Ticks)`? Why not just `new Random()` as it is effectively the same thing. – Enigmativity Aug 22 '15 at 13:07
  • Not that it is important or relevant to this question at all, but purely to see what, if any, difference it made. I didn't notice any difference from calling the parameter-less constructor, so I will just do that from now on. You don't learn anything if you don't try new things. Programming is a hobby for me, I have not had any formal education in it except for Karel the Robot, Pascal and SQL, and that was 13+ years ago, so it's important for me to tinker around, try things, break things, figure them out and learn. – cjk84 Aug 22 '15 at 13:25
  • You'd be better off downloading one of the free .NET decompilers and having a look at the source. – Enigmativity Aug 23 '15 at 12:45
  • @Enigmativity If, as you say, calling the parameterless constructor does the same thing then I can hardly see any harm, except maybe getting myself a few keystrokes closer to a case of R.S.I., but not as much closer as you have by pointlessly and condescendingly pointing it out! – cjk84 Aug 26 '15 at 04:11
  • I'm not at all being condescending. I've seen many people try to use the constructor to get around the issue of non-random numbers being produced. It doesn't work. I was trying to see if that's what you're hoping to do. – Enigmativity Aug 26 '15 at 05:35

4 Answers4

26

You should avoid using Task.Factory.StartNew with async-await. You should use Task.Run instead.

An async method returns a Task<T>, an async delegate does as well. Task.Factory.StartNew also returns a Task<T>, where its result is the result of the delegate parameter. So when used together it returns a Task<Task<T>>>.

All that this Task<Task<T>> does is execute the delegate until there's a task to return, which is when the first await is reached. If you only wait for that task to complete you aren't waiting for the whole method, just the part before the first await.

You can fix that by using Task.Unwrap which creates a Task<T> that represents that Task<Task<T>>>:

Task<Task> wrapperTask = Task.Factory.StartNew(...);
Task actualTask = wrapperTask.Unwrap();
Task.WaitAll(actualTask);
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • 1
    One more question for you: I'm guessing because I removed the async/await and as a result the task went from Task> to Task it was working purely because of that, and the change from Task.Delay to Thread.Sleep had nothing to do with it. Is that right? – cjk84 Aug 22 '15 at 12:56
  • 2
    @cjk84 yes. If you were removed the async-await and used `Task.Delay(...).Wait()` it would have had the same outcome. – i3arnon Aug 22 '15 at 12:57
  • does waitall(x).wait(timeout), waits for tasks to finishs even if timeout occure? or it'll continue – Hassan Faghihi May 31 '18 at 08:17
11

The issue with your code is that there are two tasks at play. One is the result of your Task.Factory.StartNew call, which causes the anonymous function to be executed on the thread pool. However, your anonymous function is in turn compiled to produce a nested task, representing the completion of its asynchronous operations. When you wait on your Task<Task<object>>, you're only waiting on the outer task. To wait on the inner task, you should use Task.Run instead of Task.Factory.StartNew, since it automatically unwraps your inner task:

Random rng = new Random((int)DateTime.UtcNow.Ticks);
int delay = rng.Next(1500, 15000);
Task<int> testTask = Task.Run(
    async () =>
    {
        DateTime startTime = DateTime.Now;
        Console.WriteLine("{0} - Starting test task with delay of {1}ms.", DateTime.Now.ToString("h:mm:ss.ffff"), delay);
        await Task.Delay(delay);
        Console.WriteLine("{0} - Test task finished after {1}ms.", DateTime.Now.ToString("h:mm:ss.ffff"), (DateTime.Now - startTime).TotalMilliseconds);
        return delay;
    });
Task<int>[] tasks = new[] { testTask };

Task.WaitAll(tasks);
Console.WriteLine("{0} - Finished waiting.", DateTime.Now.ToString("h:mm:ss.ffff"));

// make console stay open till user presses enter
Console.ReadLine();
Douglas
  • 53,759
  • 13
  • 140
  • 188
3

Here, Task.WaitAll waits for the outer task and not the inner task. Use Task.Run to not have nested tasks. That's the best practices solution. Another solution is to also wait for the inner task. For example:

Task<object> testTask = Task.Factory.StartNew(
    async (obj) =>
        {
            ...
        }
    ).Unwrap();

Or:

testTask.Wait();
testTask.Result.Wait();
usr
  • 168,620
  • 35
  • 240
  • 369
  • Ah, I was wondering why it wouldn't let me add the async keyword until I nested the tasks. Thank you for the info! – cjk84 Aug 22 '15 at 12:46
  • If you add an async task to the Task.Factory don't remember to add await before: await Task.Factory.StartNew(async () => .... – Lev K. Mar 07 '20 at 19:43
1

After much screwing around and hair-pulling I finally decided to get rid of the async lambda and just use the System.Threading.Thread.Sleep method, to see if that made any difference.

The new code ended up like this:

Random rng = new Random((int)DateTime.UtcNow.Ticks);
int delay = rng.Next(1500, 15000);
Task<object> testTask = Task.Factory.StartNew<object>(
    (obj) =>
        {
            DateTime startTime = DateTime.Now;
            Console.WriteLine("{0} - Starting test task with delay of {1}ms.", DateTime.Now.ToString("h:mm:ss.ffff"), (int)obj);
            System.Threading.Thread.Sleep((int)obj);
            Console.WriteLine("{0} - Test task finished after {1}ms.", DateTime.Now.ToString("h:mm:ss.ffff"), (DateTime.Now - startTime).TotalMilliseconds);
            return obj;
        },
        delay
    );
Task<object>[] tasks = new Task<object>[] { testTask };

Task.WaitAll(tasks);
Console.WriteLine("{0} - Finished waiting.", DateTime.Now.ToString("h:mm:ss.ffff"));

// make console stay open till user presses enter
Console.ReadLine();

Note: Due to removing the async keyword from the lambda method, the task type can now be simply Task<object> rather than Task<Task<object>> - you can see this change in the code above.

And, voila! It worked! I got the 'Finished waiting.' message AFTER the task completed.

Alas, I remember reading somewhere that you shouldn't use System.Threading.Thread.Sleep() in your Task code. Can't remember why; but as it was just for testing and most tasks will actually be doing something that takes time rather than pretending to be doing something that takes time, this shouldn't really be an issue.

Hopefully this helps some people out. I'm definitely not the best programmer in the world (or even close) and my code is probably not great, but if it helps someone, great! :)

Thanks for reading.

Edit: The other answers to my question explain why I had the problem I did and this answer only solved the problem by mistake. Changing to Thread.Sleep(x) had no effect. Thank you to all the people who answered and helped me out!

cjk84
  • 347
  • 1
  • 2
  • 10