3

Is necessary to lock this function, when updating the shared Array using many threads?, providing that each Arrays element is accessed (read and written), by only 1 thread at most.

Thank you.

e.g.

       public string[] UpdateArray
        (List<string> myList, Func<string,string>del)
        {
         int count = myList.Count;
         string[] myArray = new string[count];
         Parallel.For(0, count, i => myArray[i] = del(myList[i]));
         return myArray;
       }

update:

its intended use would be as follows,in a WPF sync context


        public Task<string[]> updateArrayTask()
            {
                List<string> myList = GetMyList();
                Func<string, string> del = MyDel;
                var t1=Task<string[]>.Run(() => UpdateArray(myList, del));
                return t1;   
            }

and then await this Task in an async function.

pablo
  • 57
  • 6

1 Answers1

0

It is probably unnessesary to lock, but this approach forces you to think about visibility, volatility, memory models, and all this thorny stuff. Which is quite undesirable when you are writing application code with the intention of being easy to maintain, and guaranteed to produce correct results on any CPU architecture it may run. So my suggestion is to bypass the problem by using PLINQ instead of the Parallel class:

public string[] UpdateArray(List<string> myList, Func<string, string> del)
{
    return myList
        .AsParallel()
        .AsOrdered()
        .Select(s => del(s))
        .ToArray();
}

Update: If you find that the PLINQ approach has too much overhead, you could increase the robustness of your current approach by adding a Thread.MemoryBarrier at the end. Technically it may not be required, but it can buy you peace of mind for a very small price!

Synchronizes memory access as follows: The processor executing the current thread cannot reorder instructions in such a way that memory accesses prior to the call to MemoryBarrier() execute after memory accesses that follow the call to MemoryBarrier().

To reduce further the overhead of parallelism you could use the Partitioner.Create method, so that you can work with chunky portions of the list instead of its individual elements.

public string[] UpdateArray(List<string> myList, Func<string, string> del)
{
    string[] myArray = new string[myList.Count];
    Parallel.ForEach(Partitioner.Create(0, myList.Count), range =>
    {
        for (int i = range.Item1; i < range.Item2; i++)
        {
            myArray[i] = del(myList[i]);
        }
    });
    Thread.MemoryBarrier();
    return myArray;
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Isn't `Parallel.For` part of PLINQ? – Enigmativity Jun 02 '20 at 01:41
  • @Enigmativity no. AFAIK `Parallel.For` is part of the [Task Parallel Library](https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/task-parallel-library-tpl) (TPL). PLINQ is an extension to LINQ that allows queries to be executed in parallel, by starting the query with the `AsParallel` operator. I am not sure if PLINQ is considered part of TPL or not. – Theodor Zoulias Jun 02 '20 at 02:16
  • @ Theodor Zoulias. the point of using Parallel approach is preserving order without performance penalty, are you worried about myList being modified by another Thread simultaneously?, as I understand this problem persists using PLINQ. – pablo Jun 02 '20 at 07:28
  • @ Theodor Zoulias. PLINQ itself can't modify myList AFAIK, but another thread could, or do you mean PLINQ works with a local copy? – pablo Jun 02 '20 at 07:38
  • @pablo I corrected my answer by adding the `AsOrdered` operator. The problem with your approach is not with the enumeration of `myList`. Is is with the visibility of the values stored in the `myArray` variable. Theoretically it could be possible that the thread that invoked the `UpdateArray` may not see the final state of the array, because a cached portion of it may still be reside in the local cache of the core that runs this thread. I believe that the `Parallel.For` probably completes with placing a memory barrier that makes this scenario impossible, but I wouldn't bet my life on it. – Theodor Zoulias Jun 02 '20 at 08:14
  • @pablo AFAIK PLINQ doesn't work with a local copy. It enumerates the source enumerable once, but it does so on-the-go. It doesn't start by creating a defensive copy of the enumerable. – Theodor Zoulias Jun 02 '20 at 08:18
  • @ Theodor Zoulias. I see,What do you think of my updated approach? – pablo Jun 02 '20 at 11:35
  • @pablo a better name for the `updateArrayTask` method, according to the [guidelines](https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap#naming-parameters-and-return-types), would be `UpdateArrayAsync`. Regarding offloading the parallel operation to a background thread with `Task.Run`, it's a perfectly valid approach, but doesn't change much the situation for the called `UpdateArray`. By going lock-less, you are trading peace of mind for performance. You are the only one who can judge if this tradeoff makes sense in your case. – Theodor Zoulias Jun 02 '20 at 12:22