Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 540
Ryan Burbidge Avatar asked Jul 25 '14 06:07

Ryan Burbidge


2 Answers

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.

like image 117
Jon Skeet Avatar answered Sep 27 '22 20:09

Jon Skeet


Your problem seems to lay with semaphore.WaitOne()

An async method will run synchronously until it hits its first await. In your code, the first await is only after the WaitOne is signaled. The fact that a method is async certainly does not mean it runs on multiple threads, it usually means the opposite.

Do get around this, use SemaphoreSlim.WaitAsync, that way the calling thread will yield control until the semaphore signals its done

public class Class2
{
    private readonly SemaphoreSlim semaphore = new SemaphoreSlim(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
       {
           await semaphore.WaitAsync();
           {
               // 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;
}
like image 22
Yuval Itzchakov Avatar answered Sep 27 '22 22:09

Yuval Itzchakov