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.
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:
Task.Run
, and the task scheduler creates a new thread to run it. (It may not need to, of course.)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.
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;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With