Task.WhenAll() only executes 2 threads at a time?

In this problem I am trying to cache a single value, let's call it foo. If the value is not cached, then it takes while to retrieve.

My problem is not implementing it, but testing it.

In order to test it, I fire off 5 simultaneous tasks using Task.WhenAll() to get the cached value. The first one enters the lock and retrieves the value asynchronously, while the other 4 threads should wait on the lock. After waiting, they should one by one re-check the cached value, find that it has been retrieved by the first thread that cached it, and return it without a second retrieval.

[TestClass]
public class Class2
{
    private readonly Semaphore semaphore = new Semaphore(1, 1);

    private bool? foo;

    private async Task<bool> GetFoo()
    {
        bool fooValue;

        // Atomic operation to get current foo
        bool? currentFoo = this.foo;

        if (currentFoo.HasValue)
        {
            Console.WriteLine("Foo already retrieved");
            fooValue = currentFoo.Value;
        }
        else
        {
            semaphore.WaitOne();
            {
                // Atomic operation to get current foo
                currentFoo = this.foo;

                if (currentFoo.HasValue)
                {
                    // Foo was retrieved while waiting
                    Console.WriteLine("Foo retrieved while waiting");
                    fooValue = currentFoo.Value;
                }
                else
                {
                    // Simulate waiting to get foo value
                    Console.WriteLine("Getting new foo");
                    await Task.Delay(TimeSpan.FromSeconds(5));
                    this.foo = true;
                    fooValue = true;
                }
            }
            semaphore.Release();
        }

        return fooValue;
    }

    [TestMethod]
    public async Task Test()
    {
        Task[] getFooTasks = new[] {
            this.GetFoo(),
            this.GetFoo(),
            this.GetFoo(),
            this.GetFoo(),
            this.GetFoo(),
        };
        await Task.WhenAll(getFooTasks);
    }
}

In my actual test and production code, I am retrieving the value through an interface and mocking that interface using Moq. At the end of the test I verify that the interface was only called 1 time (pass), rather than > 1 time (failure).

Output:

Getting new foo
Foo retrieved while waiting
Foo already retrieved
Foo already retrieved
Foo already retrieved

However you can see from the output of the test that it isn't as I expect. It looks as though only 2 of the threads executed concurrently, while the other threads waited until the first two were completed to even enter the GetFoo() method.

Why is this happening? Is it because I'm running it inside a VS unit test? Note that my test still passes, but not in the way I expect it to. I suspect there is some restriction on the number of threads in a VS unit test.

Jon Skeet
people
quotationmark

Task.WhenAll() doesn't start the tasks - it just waits for them.

Likewise, calling an async method doesn't actually force parallelization - it doesn't introduce a new thread, or anything like that. You only get new threads if:

  • You await something which hasn't completed, and your synchronization context schedules the continuation on a new thread (which it won't do in a WinForms context, for example; it'll just reuse the UI thread)
  • You explicitly use Task.Run, and the task scheduler creates a new thread to run it. (It may not need to, of course.)
  • You explicitly start a new thread.

To be honest, the use of blocking Semaphore methods in an async method feels very wrong to me. You don't seem to be really embracing the idea of asynchrony... I haven't tried to analyze exactly what your code is going to do, but I think you need to read up more on how async works, and how to best use it.

people

See more on this question at Stackoverflow