Why are separate method invocations considered the same Task?

While debugging an issue, I've found that short running tasks are considered equal. I'd like to know why this is, and if there is any way of preventing this behavior.

Below, I've given a unit test (using mstest) that reproduces this behavior. If you uncomment the Task.Delay and introduce a delay, the test passes.

    private async Task successTaskFn()
        //await Task.Delay(250);
        MemoryStream ms = new MemoryStream();
        using (var sw = new StreamWriter(ms))
            await sw.WriteLineAsync("Line1");
            await sw.WriteLineAsync("Line2");

    public void TaskIdsCheck()
        var t1 = successTaskFn();
        var t2 = successTaskFn();
        var t3 = successTaskFn();

        Assert.AreNotSame(t1, t2);
        Assert.AreNotSame(t1, t3);
        Assert.AreNotSame(t2, t3);

The test fails on AreNotSame, because the Task objects are considered the same.

1 answer

  • answered 2018-07-11 05:29 Daisy Shipton

    There are two important points at work here:

    • Async methods run synchronously until the first await expression that is awaiting something that hasn't already completed.
    • There are optimizations to reuse tasks where possible. This is only possible for completed tasks, and then only when the result is okay to cache. In your case you're not dealing with any tasks returning values - it's easy for "a completed task with no result" to be cached, as you only need one of them.

    So if you have a method that does nothing asynchronous (no awaits at all) and just returns Task, that will always return the same Task. (It's no guaranteed, but that's the current behaviour.)

    If you have a method that does have awaits, it will depend on what's being awaited. If the operation you await hasn't completed, the async machinery needs to do a complicated dance to box the state machine (the first time), schedule a continuation, and then return to the caller.

    In your case, you're only awaiting the result of operations on MemoryStream. There's no point in MemoryStream being asynchronous for this sort of operation, so its async methods just return a completed task - which makes it work as if it were entirely synchronous.

    If you use:

    await Task.Yield();

    that should prevent that behaviour, but I'd only do that in extreme cases. You simply shouldn't be relying on the identity of the returned task.