The collection was mutated during enumeration - archiving data and writing to a file using NSCoder

In my application, I periodically write a set of dynamic data to a file. The data object is updated every second. Sometimes I get the exception "The collection was mutated, being mutated" on one of the lines of my encodeWithCoder: method. Each object is encoded as follows:

[aCoder encodeObject:self.speeds forKey:@"speeds"]; 

Where self.speeds is an NSMutableArray. I assume that the problem is that the data is being updated while it is being encoded. I tried using @synchronize in the encoding and saving blocks, and I also tried to make the property atomic rather than non-atomic, but it didn't work. Saving occurs in the background. Any ideas on how to keep this data in the background during the update? I want to make a copy and then save a copy, but will the same problem occur? Thanks!


Change 1:

The idea of ​​the application is that I open a map view that periodically updates a singleton class and contains an array of data objects, each data object being information about the user's map. In each data object - the user's location, speed, altitude, distance, etc. Each time the location manager updates the user's location, I update the current data object (the "live" data object that was just created to track this trip, at any time there can be only one "live" data object) with new information.

I would like to write the entire singleton to a file every x minutes, but sometimes recording and updating happen at the same time, and I get this error (or at least this is what I assume causes this crash). Is there a problem here with my code or my design pattern?


This is the encoding method in my custom class:

 - (void)encodeWithCoder:(NSCoder*)aCoder { @synchronized([SingletonDataController sharedSingleton]) { [aCoder encodeObject:[[lineLats copy] autorelease] forKey:@"lineLats"]; [aCoder encodeObject:[[lineLongs copy] autorelease] forKey:@"lineLongs"]; [aCoder encodeObject:[[horizontalAccuracies copy] autorelease] forKey:@"horAcc"]; [aCoder encodeObject:[[verticalAccuracies copy] autorelease] forKey:@"vertAcc"]; [aCoder encodeObject:[[speeds copy] autorelease] forKey:@"speeds"]; [aCoder encodeObject:[[overlayColors copy] autorelease] forKey:@"colors"]; [aCoder encodeObject:[[annotationLats copy] autorelease] forKey:@"annLats"]; [aCoder encodeObject:[[annotationLongs copy] autorelease] forKey:@"annLongs"]; [aCoder encodeObject:[[locationManagerStartDate copy] autorelease] forKey:@"startDate"]; [aCoder encodeObject:[[locationManagerStartDateString copy] autorelease] forKey:@"locStartDateString"]; [aCoder encodeObject:[[mapTitleString copy] autorelease] forKey:@"title"]; [aCoder encodeObject:[[shortDateStringBackupCopy copy] autorelease] forKey:@"backupString"]; [aCoder encodeFloat:pathDistance forKey:@"pathDistance"]; [aCoder encodeFloat:linearDistance forKey:@"linearDistance"]; [aCoder encodeFloat:altitudeChange forKey:@"altitudeChange"]; [aCoder encodeFloat:averageSpeedWithFilter forKey:@"avWithFilter"]; [aCoder encodeFloat:averageSpeedWithoutFilter forKey:@"avWithoutFilter"]; [aCoder encodeInt:totalTripTimeInSeconds forKey:@"totalTimeInSecs"]; } } 

This is the update method (in the method and other methods called by the update method, there is more code, but I omit everything that does not refer to the "live" dataObject , the one that is being updated)

 - (void)locationManager:(CLLocationManager*)manager didUpdateToLocation:(CLLocation*)newLocation fromLocation:(CLLocation*)oldLocation { @synchronized([SingletonDataController sharedSingleton]) { //create temporary location for last logged location CLLocation* lastLocation; if([dataObject.lineLats lastObject] && [dataObject.lineLongs lastObject]) { lastLocation = [[CLLocation alloc] initWithLatitude:[[dataObject.lineLats lastObject] floatValue] longitude:[[dataObject.lineLongs lastObject] floatValue]]; } else { lastLocation = [oldLocation retain]; } //..... //periodically add horizontal/vertical accuracy if(iterations > 0 && iterations % 4 == 0) { [dataObject.horizontalAccuracies addObject:[NSNumber numberWithFloat:[newLocation horizontalAccuracy]]]; [dataObject.verticalAccuracies addObject:[NSNumber numberWithFloat:[newLocation verticalAccuracy]]]; } //..... //accumulate some speed data if(iterations % 2 == 0) { NSNumber* speedNum = [[NSNumber alloc] initWithFloat:[newLocation speed]]; [dataObject.speeds addObject:speedNum]; [speedNum release]; } //..... //add latitude and longitude NSNumber* lat = [[NSNumber alloc] initWithFloat:[newLocation coordinate].latitude]; NSNumber* lon = [[NSNumber alloc] initWithFloat:[newLocation coordinate].longitude]; if(fabs([lat floatValue]) > .0001 && fabs([lon floatValue]) > .0001) { [dataObject.lineLats addObject:lat]; [dataObject.lineLongs addObject:lon]; } if(iterations % 60 == 0) { [[SingletonDataController sharedSingleton] synchronize]; } } } 

And finally, the synchronize method in the SingletonDataController class (updated so that now synchronization occurs in the asynchronous block according to Tommy's answer):

 dispatch_async(self.backgroundQueue, ^{ @synchronized([SingletonDataController sharedSingleton]) { NSLog(@"sync"); NSData* singletonData = [NSKeyedArchiver archivedDataWithRootObject: [SingletonDataController sharedSingleton]]; if(!singletonData) { return; } NSString* filePath = [SingletonDataController getDataFilePath]; [singletonData writeToFile:filePath atomically:YES]; } }); 

where backgroundQueue is created as follows:

 [sharedSingleton setBackgroundQueue:dispatch_queue_create("com.xxxx.xx", NULL)]; 

I can send more code if necessary, but they seem to be important parts.

+8
asynchronous ios objective-c serialization nscoder
source share
3 answers

You execute dispatch_async on one of your @synchronize s. Material there is not subject to implicit locking built into synchronization; all that happens is that you get a lock, send a block, and then release the lock. This way, a block can easily happen outside of a block (and, indeed, you expect it to usually be).

To adhere to the synchronization path, you want @synchronize inside the block, not outside it. However, you can try to take a less decisive approach, for example, to execute all your updates in one queue of sequential sending and allow them to enter the corresponding important values ​​in the main queue.

+5
source share

If you are afraid that serialization will take a long time to affect the next serialization, copy the object, then use dispatch_async to serialize it. Thus, serialization will take place in an asynchronous queue.

However, perhaps you want to completely rethink this approach. Isn't that basic data? With it, you can only update the values ​​that have really changed, and I am sure that it will be able to cope with your lock problems.

Edit Sorry, I read your original post incorrectly. If you don’t save too often, you might want to use locks. See https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/Multithreading/ThreadSafety/ThreadSafety.html

But do this only if you are not serializing too often, as this will significantly reduce your performance.

So, lock the object, copy it, open the object, serialize async to copy.

+1
source share

Yes, serializing a copy of an array instead of a mutable array ensures that the array does not change when it is saved, but you just shift the problem: you still have a case where the array can change to one thread when it is copied to another. You can put @synchronize blocks around the copy and mutations of the array (just like you said you were doing save / update .. that should work - did you use the same object for the @synchronize parameter? @Synchronize (self ) is a convenient way to do this).

Another way to synchronize a copy operation is to use dispatch_sync () to make a copy in the main thread:

 __block NSArray* listCopy; dispatch_sync(dispatch_get_main_queue(), ^{ listCopy = [self.speeds copy]; }); [aCoder encodeObject:listCopy forKey:@"speeds"]; [listCopy release]; 

This is a bit coarser - it cannot make a copy until the main stream is clear, while @synchronized copy can work as soon as the main stream exits the @synchronize block, but it has the advantage that you only need to put this code in a save stream, not worry about where you can change the array in the main stream.

Edit: I just saw another note about using NSLock. Using @synchronize is almost the same as using NSLock ( here is a good SO post), but you don’t have to worry about managing a blocking object. Again, @synchronize should work for you, and it’s really convenient if you don’t have dozens of different places that you need to sync.

0
source share

All Articles