2

I have a dictionary where if the item in the dictionary passes through all the processing I want to remove it.

            var dictEnum = dictObj.GetEnumerator();
            while (dictEnum.MoveNext())
            {
                 Parallel.ForEach(dictObj, pOpt, (KVP, loopState) =>
                 {
                      processAndRemove(KVP.Key);
                 });
            }

            private void processAndRemove(string keyId)
            {
               try
               {
               <does stuff>
               dictObj.Remove(keyId);
               } catch(exception ex) {
                 ...
                 <does not remove anything, wants to retry until it doesn't fail>
               }
            }

I'd like for the loop to continue processing with all the remaining items (non-removed) in the dictionary.

However, I'm getting an error. When I run a simpler version of this code I get a message stating:

Collection was modified; enumeration operation may not execute

Is there a way to do this using a dictionary?

Update:

Just to give more context. The idea behind this is so the loop continues to run if there are items left in the dictObj. So if I start with 10 and 8 pass I want to re-run the 2 that didn't pass until they do.

webdad3
  • 8,893
  • 30
  • 121
  • 223

6 Answers6

4

As Jalayn says, you can't remove from a collection whilst you're enumerating it. You'd have to rewrite your code so that it added to another collection, then enumerated that collection and deleted the items from the original collection.

Something like:

var toRemove = new Dictionary<int, string>() //whatever type it is

Parallel.ForEach(dictObj, pOpt, (KVP, loopState) =>
{
    toRemove.Add(KVP);
});

foreach (var item in toRemove)
{
    dictObject.Remove(item.Key);
}
Mathew Thompson
  • 55,877
  • 15
  • 127
  • 148
  • This keeps the strange `while` loop from the question, where `.MoveNext()` is called but `.Current` is never read. Du you see what the `while` loop is supposed to mean? If `dictObj` has an initial count of 100, should there be 100 `Parallel.ForEach` method calls or what? ___Edit:___ Also, would it be safe to add to your `toRemove` dictionary of type `Dictionary<,>` in a _parallel_ loop? It is not thread-safe? – Jeppe Stig Nielsen Apr 30 '13 at 22:14
  • @JeppeStigNielsen You're right, removed that `while`. I tried not to focus on butchering OP's code too much and just answering his question about the error :). As for that parallel comment, my PLINQ ain't *that* great, but feel free to edit my post if you can further elaborate on that matter. – Mathew Thompson Apr 30 '13 at 22:25
  • see my updated question. Does this answer the one process question? – webdad3 Apr 30 '13 at 22:30
  • I'm completely sure it won't work to have many concurrent threads `Add` to the same `Dictionary<,>`. You could change the type of `toRemove` into a `System.Collections.Concurrent.BlockingCollection` for example. Note that you need only remember the keys. – Jeppe Stig Nielsen Apr 30 '13 at 22:47
3

You cannot remove an item from a collection if you are iterating over it in the same time. What you can do however is to store all elements you want to remove in a separate collection.

Then, when you're done enumerating you can iterate over your list to remove each item from the original collection.

Alternatively, check out Best way to remove multiple items matching a predicate from a c# Dictionary?. It's pretty. The accepted answer excerpt, courtesy of user @JaredPar is:

foreach ( var s in MyCollection.Where(p => p.Value.Member == foo).ToList() ) {
  MyCollection.Remove(s.Key);
}
Community
  • 1
  • 1
Jalayn
  • 8,934
  • 5
  • 34
  • 51
  • The task though is to keep the process running until I have no more items in the dictionary (or whatever container). The requirements that I'm working off of have it so it is in one process. – webdad3 Apr 30 '13 at 22:29
1

I don't think you can do it using a dictionary. Instead you can do something like Dictionary.Values.ToList(), remove what you want there then reconcile the differences.

This question has more information on it Collection was modified; enumeration operation may not execute

Community
  • 1
  • 1
evanmcdonnal
  • 46,131
  • 16
  • 104
  • 115
1

Start a second collection and add values into it that you want to keep.

Buchannon
  • 1,671
  • 16
  • 28
1

Why do you call GetEnumerator() explicitly instead of using foreach? The foreach statement is there to help you. In this case, you use MoveNext() in a loop, but you never read the Current property.

It looks like you try to use Parallel.ForEach on your dictObj, but are you sure it is of a type that is thread-safe? Probably not. What is its type?

Finally, the error text speaks for itself. You can't modify the same collection you are iterating through.

Jeppe Stig Nielsen
  • 60,409
  • 11
  • 110
  • 181
  • From my research you need the dictionary to have an enumerator when doing a while loop. – webdad3 Apr 30 '13 at 22:02
  • I'm exploring the use of the while loop so that it will continue to run through all the items that failed rather than having to start another process. – webdad3 Apr 30 '13 at 22:04
  • @JeffV What is the _type_ of `dictObj` field? – Jeppe Stig Nielsen Apr 30 '13 at 22:20
  • @JeffV If that means `System.Collections.Generic.Dictionary`, there's no hope you can modify (like add to or remove from) the object in a parallel loop (i.e. from many threads at once). It is not a thread-safe type. Do you really _need_ that parallelism? There do exist types such as `System.Collections.Concurrent.ConcurrentDictionary<,>` but I can't tell if they're relevant for your scenario. – Jeppe Stig Nielsen Apr 30 '13 at 22:35
  • I can say that I have run the dictionary in parallel and it has worked fine. This removing of items from the dictionary is new however... – webdad3 Apr 30 '13 at 22:46
  • Actually I think your comment about the ConcurrentDictionary is correct. I got it to work with the ConcurrentDictionary. – webdad3 May 01 '13 at 13:34
1

From the conversation that I had from: Jeppe Stig Nielsen spawned an idea to try the ConcurrentDictionary

This is my test code where I was able to remove the items from the Dictionary (from within the Parallel.Foreach loop) and the while loop continues until the count == 0 or the retryAttempts > 5

    public static ConcurrentDictionary<string, myRule> ccDict= new ConcurrentDictionary<string, myRule>();
       try
        {
            while (ccDict.Count > 0)
            {
                Parallel.ForEach(ccDict, pOptions, (KVP, loopState) =>
                {
                    //This is the flag that tells the loop do exit out of loop if a cancellation has been requested
                    pOptions.CancellationToken.ThrowIfCancellationRequested();
                    processRule(KVP.Key, KVP.Value, loopState);
                }); //End of Parallel.ForEach loop
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message.ToString());
            Console.ReadLine();
        }

    public static int processRule(string rId, myRule rule, ParallelLoopState loopState)
    {
        try
        {
            if (rId == "001" || rId == "002")
            {
                if (rId == "001" && ccDict[rId].RetryAttempts == 2)
                {
                    operationPassed(rId);
                    return 0;
                }
                operationFailed(rId);
            }
            else
            {
                operationPassed(rId);
            }
            return 0;
        }
        catch (Exception ex)
        {
            Console.WriteLine("failed : " + ex.Message.ToString());
            return -99;
        }
    }

    private static void operationPassed(string rId)
    {
        //Normal Operation
        ccDict[rId].RulePassed = true;
        ccDict[rId].ExceptionMessage = "";
        ccDict[rId].ReturnCode = 0;

        Console.WriteLine("passed: " + rId + " Retry Attempts : " + ccDict[rId].RetryAttempts.ToString());

        rule value;
        ccDict.TryRemove(rId, out value);
    }

    private static void operationFailed(string ruleId)
    {
        //This acts as if an EXCEPTION has OCCURED
        int retryCount = 0;

            ccDict[rId].RulePassed = false;
            ccDict[rId].RetryAttempts = ccDict[rId].RetryAttempts + 1;
            ccDict[rId].ExceptionMessage = "Forced Fail";
            ccDict[rId].ReturnCode = -99;

            ccDict.TryUpdate(rId, ccDict[rId], ccDict[rId]);

            if (ccDict[rId].RetryAttempts >= 5)
            {
                Console.WriteLine("Failed: " + rId + " Retry Attempts : " + ccDict[rId].RetryAttempts.ToString() + " : " + ccDict[rId].ExceptionMessage.ToString());
                cancelToken.Cancel();
            }
    }

    public class myRule
    {
        public Boolean RulePassed = true;
        public string ExceptionMessage = "";
        public int RetryAttempts = 0;
        public int ReturnCode = 0;


        public myRule()
        {
            RulePassed = false;
            ExceptionMessage = "";
            RetryAttempts = 0;
            ReturnCode = 0;
        }
    }
webdad3
  • 8,893
  • 30
  • 121
  • 223