Search code examples
c#refoutparallel.foreachparallel.for

how to access a ref or out parameter from Parallel.For or Parallel.ForEach lambda expression


Consider my following (simplified) code:

public double ComputeSum(List<double> numbers, ref double threshold, Object thresholdLock)
{
    double sum = 0;
    Object sumLock = new Object();

    Parallel.ForEach (numbers, (number) => 
    {
        bool numberIsGreaterOrEqualThanThreshold;
        lock (thresholdLock)
        {
            numberIsGreaterOrEqualThanThreshold = number >= threshold;
        }
        if (numberIsGreaterOrEqualThanThreshold)
        {
            lock (sumLock)
            {
                sum += number;
            }
        }   
    });
    return sum;
}

This code does not compile. The compiler error message is:

Cannot use ref or out parameter 'threshold' inside an anonymous method, lambda expression, or query expression

The goal of this parallel ComputeSum method is to parallely compute the sum of some numbers of the 'numbers' parameter list. This sum will include all the numbers that are greater or equal to the referenced threshold ref parameter.

This threshold parameter is passed as a ref because it can be modified by some other tasks during the ComputeSum method execution, and I need each number comparaison to be made with the current threshold value at the time at which the comparaison with the threshold is made. (I know, in this simplified example it may appear silly to do this but the actual code is more complex and makes sense).

My question is: What workaround can I use to access the threshold by ref inside the Parallel.ForEach lambda-expression statement ?

Note: I read the "said duplicate" question Cannot use ref or out parameter in lambda expressions but I'm not asking why this ref parameter access is refused by the compiler but I'm asking for a workaround to do what I intend to do.


Solution

  • Here is the solution I found:

    The point was to wrap the shared double value (threshold) into a class (that can also implement mutual exclusion) and pass this object as parameter to the parallel computing method including the Parallel.ForEach statement.

    The code is now more clear and works like I intended. (Each access to the threshold value refers the last updated value.)

    The generic SharedVariable<T> class protects a value of any type against concurrent reading/writing threads.

    Note the use of a ReaderWriterLockSlim lock to prevent the readers to lock themselves when concurrently reading the variable's value.

    (Only Writer threads require exclusive access to the variable's value).

    public class SharedVariable<T>
    {
        // The shared value:
        private T value;
        
        // The ReaderWriterLockSlim instance protecting concurrent access to the shared variable's value:
        private ReaderWriterLockSlim readerWriterLock = new ReaderWriterLockSlim();
        
        // Constructor
        public SharedVariable(T val)
        {
            this.value = val;
        }
    
        // Gets or sets the value with thread-safe locking and notifying value changes 
        public T Value 
        {
            get
            {
                readerWriterLock.EnterReadLock();
                try
                {
                    return value;
                }
                finally
                {
                    readerWriterLock.ExitReadLock();
                }
            }
        
            set
            {
                readerWriterLock.EnterWriteLock();
                try
                {
                    if (!this.value.Equals(value))
                    {
                        this.value = value;
                    }
                }
                finally
                {
                    readerWriterLock.ExitWriteLock();
                }
            }
        }
        
        // GetAndSet allows to thread-safely read and change the shared variable's value as an atomic operation. 
        // The update parameter is a lamda expression computing the new value from the old one. 
        // Example: 
        // SharedVariable<int> sharedVariable = new SharedVariable<int>(0);
        // sharedVariable.GetAndSet((v) => v + 1);  // Increments the sharedVariable's Value.
        public void GetAndSet(Func<T,T> update)
        {
            readerWriterLock.EnterWriteLock();
            try
            {
                T newValue = update(this.value);
                if (!this.value.Equals(newValue))
                {
                    this.value = newValue;
                }
            }
            finally
            {
                readerWriterLock.ExitWriteLock();
            }
        }
    }
    
    public double ComputeSum(List<double> numbers, SharedVariable<double> thresholdValue)
    {
        SharedVariable<double> sum = new SharedVariable<double>(0);
    
        Parallel.ForEach (numbers, (number) => 
        {
            if (number >= thresholdValue.Value)
            {
                sum.GetAndSet((v) => v + number);
            }   
        });
        return sum.Value;
    }