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.

¿Fue útil?

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

  1. just increases the load on that external service with those costly parallel calls
  2. 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.

So, perhaps, a simpler single-threaded approach is a better choice.

Licenciado bajo: CC-BY-SA con atribución
scroll top