The question may sound a bit abstract, here's an example of a non async piece of code that does some lazy loading and ensures that the lazy loading is only done once by the first thread:
public class LazyExample
{
private object ExpensiveResource = null;
private object ExpensiveResourceSyncRoot = new object();
public object GetExpensiveResource()
{
if (ExpensiveResource != null) // Checkpoint 1
return ExpensiveResource;
lock (ExpensiveResourceSyncRoot) // initially there will be a queue of threads waiting here that have already passed Checkpoint 1
{
if (ExpensiveResource != null) // prevent re-retrieval by all subsequent threads that passed Checkpoint 1
return ExpensiveResource;
// create the expensive resource but do not assign yet...
object expensiveResource = new object();
// initialize the expensive resource, for example:
// - Call an API...
// - Do some Database lookups...
Thread.Sleep(1);
// finally as last step before releasing the lock, assign the fully initialized expensive object
ExpensiveResource = expensiveResource;
}
return ExpensiveResource;
}
}
In our lib, the async virus has started to infest many calls. Since awaiting is not allowed directly inside a lock we now wrap the async stuff in a new method like so:
public class LazyExample
{
private object ExpensiveResource = null;
private object ExpensiveResourceSyncRoot = new object();
public async Task<object> GetExpensiveResourceAsync()
{
if (ExpensiveResource != null) // Checkpoint 1
return ExpensiveResource;
lock (ExpensiveResourceSyncRoot) // initially there will be a queue of threads waiting here that have already passed Checkpoint 1
{
if (ExpensiveResource != null) // prevent re-retrieval by all subsequent threads that passed Checkpoint 1
return ExpensiveResource;
// assign the fully initialized expensive object
ExpensiveResource = CreateAndInitializeExpensiveResourceAsync().Result;
}
return ExpensiveResource;
}
private async Task<object> CreateAndInitializeExpensiveResourceAsync()
{
object expensiveResource = new object();
// initialize the expensive resource, this time async:
await Task.Delay(1);
return expensiveResource;
}
}
This however feels like putting a zip-tie around a safety latch and defeating the cause.
In seemingly random cases we need to wrap a call in order to prevent deadlocks like so:
ExpensiveResource = Task.Run(CreateAndInitializeExpensiveResourceAsync).Result;
This will force the method to run in a different thread and cause the current thread to go idle until it joins (extra overhead for no good reason as far as I can tell).
So my question is: is it safe to offload async stuff to a separate method (a new stack frame if you will) inside a lock or are we indeed defeating a safety latch?