I am currently implementing a multithreaded application and I encounter a strange race condition (leading to an ARC problem: error for object 0x7f8bcbd6a1c0: pointer being freed was not allocated).
The app creates multiple NSOperations, each one is downloading and processing information. As in every one of these NSOperations an error may occur (e.g., the web service is not available), I want to propagate the error up so I can handle it. However, I seem to encounter a race condition and the operations try to access invalid memory.
A minimalistic example which shows the memory access problem is the following one:
- (void) createError: (NSError**) err {
*err = [[NSError alloc] initWithDomain:@"Test" code:12345 userInfo:nil];
}
- (void)viewDidLoad {
[super viewDidLoad];
__block NSError *globalError = nil;
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSOperation *op = nil;
for (int i=0; i<10; i++) {
op = [NSBlockOperation blockOperationWithBlock:^{
NSError *err = nil;
[self createError:&err];
// This loop increases the chance to get the race condition
for (int j=0; j<10000;j++) {
@synchronized(globalError) {
globalError = err;
}
}
}];
[queue addOperation:op];
}
[queue waitUntilAllOperationsAreFinished];
if (globalError) {
NSLog(@"An error occured in at least one of the operations");
}
}
Whenever I run this program, I get an exception. Mostly it is error for object 0x7fc0b860e860: pointer being freed was not allocated, however, sometimes I also got an EXC_BAD_ACCESS (code=EXC_I386_GPFLT) break in the debugger.
I have added the for-Loop over 10000 iterations only to increase the chance that the race condition occurs. If I leave it, the error only occurs in rare occasions (but still has to be fixed, obviously I am doing something wrong here).
The @synchronized
directive uses the object you pass to control the synchronization, so, as you note, you're trying to synchronize on nil
. And even if it wasn't nil
, the object referenced by the @synchronized
directive is a different object every time, largely defeating the attempt to synchronize.
The easiest change is to use @synchronized(self)
, instead. Or, create a NSLock
object and then lock
and unlock
it when you access globalError
. Or, if this was a high demand situation, use GCD queue (either serial queue or reader-writer pattern).