15

I'm trying to get my head around this code:

 [TestFixture]
 public class ExampleTest
 {
    [Test]
    public void Example()
    {
        AwaitEmptyTask().Wait();
    }

    public async Task AwaitEmptyTask()
    {
        await new Task(() => { });
    }
 }

The method Example never ends and blocks forever. Why??

The fix (from Stubbing Task returning method in async unit test) is to replace await new Task( () => {}) with return Task.FromResult<object>(null); but again, why is this necessary?

I know there are a bunch of questions similar to this one, but none that I've seen seem to explain why this is happening:

Community
  • 1
  • 1
Philip Pittle
  • 11,821
  • 8
  • 59
  • 123
  • I suggest you to use `Task.Factory.StartNew` to not forget to start a task – Alex Zhukovskiy Dec 04 '14 at 15:59
  • 4
    @AlexJoukovsky I suggest he does not use `Task.Factory.StartNew` and uses `Task.Run` instead. [There are issues with using StartNew](http://blog.stephencleary.com/2013/08/startnew-is-dangerous.html). – Scott Chamberlain Dec 04 '14 at 16:00

3 Answers3

23

You're creating a task and never starting it, so it never finishes.

You should be using Task.Run to create a task that you want to start executing immediately, rather than using the Task constructor.

Servy
  • 202,030
  • 26
  • 332
  • 449
  • 3
    Doesn't the `await` keyword start the task? – Philip Pittle Dec 04 '14 at 16:00
  • 3
    It attaches a continuation to the task. Nothing more. You can tell from your own code that it clearly *doesn't* start the task. If it did, then your code would actually complete. – Servy Dec 04 '14 at 16:00
  • 1
    If it only attaches a `continuation` any idea why the runtime doesn't throw an exception to let you know that you are `await`-ing an unstarted task? Is there a use case where this behavior would be desirable? – Philip Pittle Dec 04 '14 at 16:05
  • 4
    @PhilipPittle There's no reason you can't add a continuation to an unstarted task. It has no way of knowing that you never plan to ever start the task. It assumes that if you're adding a continuation to an unstarted task that you'll start it eventually, if you want the continuation to run at some point. By using the constructor you went out of your way to say, "I don't want this to start yet, but don't worry, I'll start you eventually when it's time". If that's not what you want to happen, then you shouldn't be using the constructor. – Servy Dec 04 '14 at 16:06
  • Is it better to use `Task.Run` instead of `Task.FromResult(null)`? – Philip Pittle Dec 04 '14 at 16:15
  • 1
    @PhilipPittle They do radically different things. Are you trying to create an already completed task who's result is an already known value, or are you trying to execute a method in a thread pool thread? Which you want to do determines which method you want to call. – Servy Dec 04 '14 at 16:16
  • In this scenario I'm trying to "stub" out an async method. I don't really want anything to happen. Basically, I have another class (ie `Controller`) that has a dependency on `ExampleTest.AwaitEmptyTask()`. I'm trying to unit test `Controller` and so I want `AwaitEmptyTask()` to do nothing. – Philip Pittle Dec 04 '14 at 16:19
  • @PhilipPittle Again, each operation does something very different. Use whichever one represents the operation you're trying to perform, or in this case, emulate. – Servy Dec 04 '14 at 16:22
10

You need to call Start() on your task.

Otherwise, it will never finish.

SLaks
  • 868,454
  • 176
  • 1,908
  • 1,964
  • 1
    Doesn't the `await` keyword start the task? – Philip Pittle Dec 04 '14 at 15:59
  • 1
    @PhilipPittle no, it only waits for the task to finish. – Scott Chamberlain Dec 04 '14 at 15:59
  • 1
    @SLaks - Could I trouble you to update this. Where exactly would I call `Start()`? In `Example()` or `AwaitEmptyTask()`? Calling `AwaitEmptyTask().Start();` in `Example()` throws `System.InvalidOperationException : Start may not be called on a promise-style task.` – Philip Pittle Dec 04 '14 at 16:10
  • 1
    @PhilipPittle Just don't. You don't want to create a task without starting it in the first place. Call `Task.Run` instead of using the constructor. – Servy Dec 04 '14 at 16:11
  • 1
    @PhilipPittle: You need to store the `Task` in a variable, call `Start()`, _and then_ `await` it. However, you should call `Task.Run()` instead. – SLaks Dec 04 '14 at 16:11
10

Even if you changed it to

await Task.Run(() => { });

it may still never complete. When you hit this line, the program creates a continuation task that will continue when the task completes. The fact that the task is empty is irrelevant. At that point, control flow goes back to

AwaitEmptyTask().Wait();

which waits for the task to complete and blocks the current thread.

When the task does complete, it attempts to continue on the current thread. However, it can't, because the thread is blocked by the call to Wait().

Change the awaited line to

await Task.Run(() => { }).ConfigureAwait(false);

This is a common deadlocking problem. Don't mix Wait() and await. See http://msdn.microsoft.com/en-us/magazine/jj991977.aspx

bornfromanegg
  • 2,826
  • 5
  • 24
  • 40
  • This assumes that there's a single threaded synchronization context. That's *probably* not the case here, but it is indeed a poor design for exactly this reason. – Servy Dec 04 '14 at 16:08
  • @Servy - I'm not sure I follow you. Could you elaborate? – Philip Pittle Dec 04 '14 at 16:11
  • If `SynchronizationContext.Current` is empty, or the default sync context, then the fact that the caller is waiting isn't preventing the continuation from running. He would need to call `Example` from, say, the UI thread of a desktop application with a message loop for this to happen. Being a test method, that's likely not the case; it's likely just a console app with no (or the default) sync context. – Servy Dec 04 '14 at 16:13
  • What does the `ConfigureAwait` line doe? – Philip Pittle Dec 04 '14 at 16:26
  • 2
    @PhilipPittle It configures the continuation task to continue on any available thread. By default, it will attempt to continue on the same thread, hence the above issue. – bornfromanegg Dec 04 '14 at 16:49
  • 1
    @user1158174 That's not quite right. It tells it to use the default synchronization context, rather than using the current synchronization context for the continuation. The current synchronization context may or may not involve the current thread handling all posted operations. In this case, it probably wouldn't, as it's likely called with a default synchronization context. The default context doesn't allow any thread to handle it, rather (in its current implementation) it sends the request to the thread pool. – Servy Dec 04 '14 at 17:27