Search code examples
objective-cmultithreadingmemory-managementdictionaryexc-bad-access

How to avoid EXC_BAD_ACCESS with NSMutableDictionary in non-ARC world?


According to the code below, ...

// Init a dictionary
NSMutableDictionary *dic = [NSMutableDictionary alloc] init];
[dic setObject:anObj forKey:@"key"]

...

// A : run on thread X
OBJ *obj = [dic objectForKey:@"key"];
[obj doSomething];

...

// B : run on thread Y
[dic removeObjectForKey:@"key"];

If A and B are run on different threads, X and Y, EXC_BAD_ACCESS can occur, right ?. Because :

  1. -[NSDictionary objectForKey:] doesn't retain and autorelease the returned value.
  2. -[NSMutableDictionary removeObjectForKey:] does release the object.

[obj doSomething]; will crash if [dic removeObjectForKey:@"key"]; is executed first.

A question pops up :

How can we avoid this ?

My first idea is that to retain the return value of -[NSDictionary objectForKey:], but it is not enough. Why ? Because of the 2 reasons above, there is a duration that the returned value is only retained by the dictionary. If B is executed in that duration, it will crash definitely.

// A : run on thread X
OBJ *obj = [[[dic objectForKey:@"key"] retain] autorelease];
[obj doSomething];

...

// B : run on thread Y
[dic removeObjectForKey:@"key"];

So, only retain doesn't work. RIP the 1st idea

The second idea is that to serialize them.

// A : run on thread X
@synchronized(dic) {
  OBJ *obj = [[dic objectForKey:@"key"] retain] autorelease];
}
[obj doSomething];

...

// B : run on thread Y
@synchronized(dic) {
  [dic removeObjectForKey:@"key"];
}

If I do it like this, it will work fine.

The 3rd idea is that over-retain all objects in the dictionary and release them when remove from the dictionary or dealloc the dictionary.

// Init a dictionary
NSMutableDictionary *dic = [NSMutableDictionary alloc] init];
[anObj retain];
[dic setObject:anObj forKey:@"key"]

...

// A : run on thread X
OBJ *obj = [dic objectForKey:@"key"];
[obj doSomething];

...

// B : run on thread Y
OBJ *obj = [dic objectForKey:@"key"];
[dic removeObjectForKey:@"key"];
[obj release];

Here are my questions :

  • How can people in non-ARC world use NSMutableDictionary without facing EXC_BAD_ACCESS?
  • Did they always use single thread ?
  • Did they always use lock or synchronization like I demonstrated above ?
  • And also, in ARC world, can this situation happen ?
  • Or my assumptions are all wrong ?

Solution

  • NSMutableDictionary is not thread safe. The retains on obj are just one piece of the problem. You cannot safely mutate a dictionary while reading it on separate threads, even if you are accessing different elements. You can get garbage. It is not a thread-safe container. This has nothing to do with ARC.

    There are many solutions to this problem. In many cases, an NSMutableDictionary is the wrong tool anyway. Many uses of it are better served with a small value object. Using a value object allows you to control the thread-safety through various means.

    Often the best tool is GCD. Jano provides a good example in Multi-threaded Objective-C accessors: GCD vs locks. I recommend value objects, but the same technique can be used to wrap an NSMutableDictionary if necessary. It permits parallel reads and non-blocking writes, without requiring kernel locks. It is high performance and not difficult to implement.

    Another technique I've used successfully is to switch to immutable data structures. Use an NSDictionary rather than an NSMutableDictionary. There are two ways to do this:

    • Store a mutable dictionary, but pass an immutable copy to other threads or callers, or
    • Store an immutable dictionary. When you need to mutate it, replace it instead.

    For reasonable-sized dictionaries that don't mutate that often, this can be much faster than you might imagine, and it makes thread-safety much simpler. Remember that copying an immutable objects is almost free (it's generally implemented as a retain).