Throttling the factory function of a Lazy<T> instantiated with LazyThreadSafetyMode.PublicationOnly
https://softwareengineering.stackexchange.com/questions/395078
-
28-02-2021 - |
Pregunta
When you use the constructor of Lazy<T>
requesting the valueFactory
and mode
parameters (I mean this one) you can specify LazyThreadSafetyMode.PublicationOnly
.
This way you can prevent the Lazy instance of caching any exception thrown by valueFactory
, but at the same time you lose the ability of guarantee that the factory function is executed only once. The documentation states (bold is mine):
When multiple threads try to initialize a Lazy instance simultaneously, all threads are allowed to run the initialization method (or the default constructor, if there is no initialization method). The first thread to complete initialization sets the value of the Lazy instance.
Sometimes this is fine, some other times it's better to avoid this or, at least, to limit the concurrent execution of the factory function (imagine a thousand threads concurrently executing a database query or calling a web service). I usually implement this kind of constrained concurrency by using the SemaphoreSlim class.
My question is really simple:
is there any kind of issue or contradiction in employing a SemaphoreSlim
(or a similar mechanism) in order to limit the concurrent execution of the factory function for a Lazy<T>
instance having LazyThreadSafetyMode.PublicationOnly
as its thread-safety mode ?
I can't see any issue, but I prefer asking the community because I'm not an expert.
Solución
As there is no standard way to get such a behaviour, and if you really need it, then by all means that is a valid approach.
It may even be desirable to implement your own helpers to facilitate easier/more explicit use of this pattern, either with standard Lazy or your own special-tailored LimitedConcurrencyNoErrorCachingLazy.
public class LimitedConcurrencyNoErrorCachingLazyFactory
{
private Func<Int32, Int32, SemaphoreSlim> _createExternallyOwnedSemaphore;
/// <param name="createExternallyOwnedSemaphore"> A factory to create externally owned(meaning that they are not owned/disposed by this instance) semaphore with specific initial and max count parameters.</param>
public LimitedConcurrencyNoErrorCachingLazyFactory(Func<Int32, Int32, SemaphoreSlim> createExternallyOwnedSemaphore)
{
this._createExternallyOwnedSemaphore =
createExternallyOwnedSemaphore ??
throw new ArgumentNullException(nameof(createExternallyOwnedSemaphore));
}
public Lazy<T> Create<T>(Func<T> valueFactory, Int32 maxConcurrency = 1)
{
if (valueFactory == null)
{
throw new ArgumentNullException(nameof(valueFactory));
}
if (maxConcurrency < 1)
{
throw new ArgumentOutOfRangeException(nameof(maxConcurrency));
}
var semaphore = this._createExternallyOwnedSemaphore(maxConcurrency, maxConcurrency);
return new Lazy<T>(
this.CreateLimitedConcurrencyValueFactory(semaphore, valueFactory),
LazyThreadSafetyMode.PublicationOnly);
}
public Func<T> CreateLimitedConcurrencyValueFactory(SemaphoreSlim semaphore, Func<T> valueFactory)
{
return () =>
{
semaphore.Wait();
try
{
return valueFactory();
}
finally
{
semaphore.Release();
}
}
}
}
The only thing that seems slightly off is that limiting concurrency to something more than one thread for the mentioned
imagine a thousand threads concurrently executing a database query or calling a web service
does not seem very useful as it
- just increases the load on that external service with those costly parallel calls
- with only a small and basically unintended increase of reliability of your application
- As you still have to use some retry strategy for jobs/sessions that have failed to retrieve
Lazy.Value
even if some part of them may have succeeded.
- As you still have to use some retry strategy for jobs/sessions that have failed to retrieve
So, perhaps, a simpler single-threaded approach is a better choice.