Search code examples
c#multithreadingrecursiontask-parallel-librarytaskfactory

Thread Safe Counter used by Recursive Call to Function


While using a global integer named GlobalID that I use as a counter, the following code works fine:

MyFunction(myClass thisClass, Int ID)
{   
     ListOfMyClass.add(thisClass)

     If (thisClass.IsPropertyValueAboveZero)
     {
          ID = GlobalID;
     }

     GlobalID++;

     Foreach (class someClass in ListOfClasses)
     {
           MyClass newClass = new MyClass(GlobalID, ID)
           MyFunction(newClass, ID)  //Recursive Call
     }
 }

I'm basically using GlobalID as a incrementing counter for each time the function is called. The counter is assigned a starting position for the first time it executes. I'm using a global variable because I wanted to make sure the ID is accurately increased for each pass regardless of execution entering or leaving the recursive call. This function is called (the first time....) from a ForEach loop that assigns the start position of global variable

My goal is to use a Parallel.ForEach for the initial call instead of the regular For Each loop. My problem deals with the counter. I don't know how to manage that counter within multiple threads. If I pass it as a variable to the function, I believe I will have a inaccurate lower / used number leaving the recursive loop. The global variable ensures the next number is higher than the previous number. The thisClass.IsPropertyValueAboveZero is just a arbitrary means of describing action based on a conditional statement. It has no meaningful reference to the rest of the code.

If I have multiple threads that have different starting positions for their counter, how do I accomplish making this thread safe? The only way I see at the moment is manually writing multiple versions of the same function and counter and using TaskFactory


Solution

  • For thread-safe counting, use the Interlocked methods: System.Threading.Interlocked.Increment(ref globalID)

    private static int globalID;
    
    // Be very careful about using a property to expose the counter
    // do not *use* this value directly, but can be useful for debugging
    // instead use the return values from Increment, Decrement, etc.
    public int UnsafeGlobalID { get { return globalID; } }
    

    Note: you need to use a field for the ref. It is possible to expose the field through a property, but can be problematic in code. It is better to use Interlock methods such as Interlocked.CompareExchange or explicit lock statements around logic requiring the value in a synchronized manner. The return value for Increment, Decrement, and so on is usually what should be used inside logic.

    My problem deals with the counter. I don't know how to manage that counter within multiple threads. If I pass it as a variable to the function, I believe I will have a inaccurate lower / used number leaving the recursive loop. The global variable ensures the next number is higher than the previous number.

    If I have multiple threads that have different starting positions for their counter, how do I accomplish making this thread safe? The only way I see at the moment is manually writing multiple versions of the same function and counter and using TaskFactory

    The recursive call to MyFunction(newClass, ID) will have the value of ID, but I'm not sure what If (thisClass.IsPropertyValueAboveZero) is supposed to do though. Is it to make sure you have a non-zero starting point? If so, it would be better to make sure it is non-zero before the initial call outside of this function.

    Also the logic in the foreach loop doesn't make sense to me. In MyClass newClass = new MyClass(GlobalID, ID) the ID will be argument value, or if IsPropertyValueAboveZero is true it will be the current value of GlobalID. So ID will typically be less than GlobalID since GlobalID is incremented before the foreach loop. I think you are passing GlobalID when you do not need to.

    // if IsPropertyValueAboveZero is intended to start above zero
    // then you can just initialize the counter to 1
    private static int globalID = 1;
    
    public void MyFunction(myClass thisClass, int id)
    {
        // ListOfMyClass and ListOfClasses are probably not thread-safe
        // and you may need to add locks around the Add and get a copy
        // of ListOfClasses before the foreach enumeration
        // You may want to look at https://stackoverflow.com/a/6601832/29762
        ListOfMyClass.Add(thisClass); 
    
        foreach (class someClass in ListOfClasses)
        {
            int newId = System.Threading.Interlocked.Increment(ref globalID);
            MyClass newClass = new MyClass(newId);
            MyFunction(newClass, newId);  //Recursive Call
        }
    }