As if its not obvious from my question, first time trying to work with multi-threading and need some help. I have a method that is now being called by multiple threads concurrently and it is being passed a variable (defined as shown below). The part I am having problems with is the updating of the "counter" (numProcessed) part of the custom List var, which is supposed to keep track of the total number of rows processed across all threads. I assume, this should likely be a class instead of this overcomplicated List structure, but trying to work within the current confines.
DoWorkMethod(List<(String dataSetCode, int dataSetNum, int rowCount, double modVal,
int numProcessed, List<Task> queryTasks)> lstDataSetCdTasks)
//CS1612 Error Workaround
var temp = lstDataSetCdTasks[dataSetNum];
//Interlocked.Exchange(
// ref temp.numProcessed,lstDataSetCdTasks[dataSetNum].numProcessed + 1);
temp.numProcessed = lstDataSetCdTasks[dataSetNum].numProcessed + 1;
lstDataSetCdTasks[dataSetNum] = temp;
currRow.Cells["Extract"].Value = lstDataSetCdTasks[dataSetNum].numProcessed /
lstDataSetCdTasks[dataSetNum].modVal + "| % (" +
lstDataSetCdTasks.ElementAt(dataSetNum).queryTasks.Count + " threads)";
What is the best way to update this counter in a thread safe manner? I thought I needed to use Interlocked.Exchange() given I cant update lstDataSetCdTasks directly and I have to assign that first to some temp variable then back in order to work around the CS1612 error. However, it still seems to fail to update very sporadically. Thanks in advance.
The
List<T>class is thread-safe only for multiple readers. As long as you add a writer in the mix, you must synchronize all interactions with the collection, otherwise its behavior is undefined. The simplest synchronization tool is thelockstatement. This statement requires a lockerobject, which can be any reference type, and usually is either a dedicatednew object()or the list itself. The locker should not "leak" to the outside world, so using the list itself as the locker is only viable if the list is internal, and you don't expose it to unknown code.For demonstration purposes I'll show an example with a simpler list than your
lstDataSetCdTasks, a list that contains value tuples with just two members. I am showing three approaches of incrementing theNumProcessedmember . Take a look at them, and I'll explain them below:Output:
Online demo.
The first approach locks on the
list, creating a protected region that only one thread can enter at a time¹. Inside the protected region we store a copy of a tuple in atempvariable, we mutate the copy, and then we replace the existing tuple in the list with the mutated copy. This is the simplest way to mutate a value-type stored in aList<T>.The second approach again locks on the
list, and then uses the advancedCollectionsMarshal.AsSpanto get aSpan<T>representation of the list. With theSpan<T>you gain direct access to the backing array of the list, and so you can mutate the stored value-tuples in-place, without using temporary variables. This is the most efficient way of mutating value-types stored in aList<T>.The third approach doesn't use the
lockstatement, and instead attempts to grab theSpan<T>and then mutate an entry with theInterlocked.Incrementmethod. This is valid C# code, but it is not thread-safe and it has undefined behavior. The problem is that another thread might perform concurrently an action that will replace the backing array of the list, in which case the mutation performed by the current thread will be lost. This would be a valid approach though if instead of a list you stored your tuples in an array ((string DataSetCode, int NumProcessed)[]). The arrays have fixed length, so they are less versatile than lists. But they open some opportunities for lock-free multithreading, opportunities that lists totally lack. I am not advising you to pursue these opportunities though. As a beginner in multithreading, it's much safer to stick with thelock. As long as you don't do anything heavy inside the protected regions, thelocks are cheap and won't slow down your application.¹ Provided that all other interactions with the list are performed in
lockregions protected with the same locker. Caution: even a single unprotected interaction with the list, even reading theCountproperty, renders your program invalid and it's behavior undefined. Enumerating the list should also be enclosed in a protected region. Interacting with theList<T>.Enumeratorcounts as interacting with the list itself.