2

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 ?
3329
  • 1,411
  • 13
  • 17
  • I'm pretty sure you can also get the same issue in ARC world. – JeremyP May 28 '14 at 15:16
  • Thank for your confirmation, I think so too :D. I added the _non-ARC_ keyword because I'm not sure what ARC does for me. – 3329 May 28 '14 at 15:34
  • Your assumption #2 is wrong: why do you think objectForKey does not retain autorelease your value? The whole code looks thread safe to me from a retain count point of view. Am I missing something? – alediaferia May 28 '14 at 15:42
  • @AlessandroDiaferia try it yourself, with [this code](http://pastebin.com/z7RSWdNw) – 3329 May 28 '14 at 16:06
  • @AlessandroDiaferia forgot to tell you that it is non-arc code. – 3329 May 28 '14 at 16:35
  • @AlessandroDiaferia this is ARC code, if you want to try : http://pastebin.com/HsiZSX4J. – 3329 May 28 '14 at 16:52
  • That is weird. Thank you for your example. That opened my mind. I'd like to find some official documentation about it. I'm always expecting returned object to be retained AND autoreleased, at least in non-ARC envs. – alediaferia May 29 '14 at 08:35

2 Answers2

3

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).

Community
  • 1
  • 1
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • First, thank for the answer. _GCD accessor approach_, I've tried it once and it worked great until it hit OS' thread limit. That when deadlock can happen. _Copy on write_ is another method I've tried. There is a critical section when changing the dictionary to the new one, in the case I store an immutable one. So lock is required. Even another case, if removing and copying are happen concurrently, Still, EXC_BAD_ACCESS is possible. Again, lock is require. Maybe your first suggestion is the base, `NSMutableDictionary` is the wrong tool. So avoid using it. I'll keep that in mind. – 3329 May 28 '14 at 15:16
  • "GCD accessor approach, I've tried it once and it worked great until it hit OS' thread limit." This doesn't make sense. GCD queues are independent of threads. Yes, swapping the dictionary still must be done in a thread-safe way (setting properties is not thread-safe either). As a side note: there should no longer be a "non-ARC world." The number of places that non-ARC code is appropriate has been close to zero for several years now. I wrote this in 2012 and it's only become more true: http://stackoverflow.com/questions/8760431/to-arc-or-not-to-arc-what-are-the-pros-and-cons/8760820#8760820 – Rob Napier May 28 '14 at 16:18
  • About GCD, it's not independent to threads. I think it is only give the control about threads to the system. As it is stated in GCD Apple Doc: _Blocks submitted to dispatch queues are executed on a pool of threads fully managed by the system_ . If you have a concurrence queue and dispatch a lot of tasks into it. GCD will create a thread to run each task. MacOSX default thread limit per process is only 64. So, it's not that hard to reach the limit. About the ARC, even in ARC, this EXC_BAD_ACCESS problem can happen but it is harder. I ran [this code](http://pastebin.com/geBQzZQ9) and it crashed. – 3329 May 28 '14 at 16:47
  • Yes; ARC does not prevent bad access exceptions. I'm just saying that there are very few cases today where ARC is not strongly recommended. And yes, it is possible to overwhelm GCD with parallel requests, but the parallel accessor pattern shouldn't do that, unless you're reading from properties *much* more often than you should be. – Rob Napier May 28 '14 at 18:57
  • I agree about using ARC and I currently use ARC nowadays. I asked these questions about non-ARC environment because of pure curiosity. About the accessor pattern, I just want to point out that GCD is not the silver bullet. It can lead to a new problem if use it carelessly, as I did. ps. Thank you to be patient with me. If I said something sound harsh to you, I am very sorry. I’m not a native English user, still learning it. – 3329 May 28 '14 at 21:09
  • No problem at all. It's a good question and it is certainly possible for GCD to run away on you if you aren't careful as you say. (It's easy to tell your English isn't native, but it's quite good. That's no issue at all. You make no more mistakes than native speakers; you just make different ones :D) – Rob Napier May 28 '14 at 21:48
0

If you need to remove the object from the dictionary after [obj doSomething] then it means you do not need concurrency, so simply do that in the same thread.

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

Otherwise you could use a NSCondition object

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

// Init the condition object
NSCondition *myCondition = [[NSCondition alloc] init];

...

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

...

// B : run on thread Y
[myCondition wait]; // will wait until thread X calls signal
[dic removeObjectForKey:@"key"];
alediaferia
  • 2,537
  • 19
  • 22
  • The need to remove an object from another thread may be required in some cases, such as delete when receive a signal like when implementing a kind of cache. The other suggestion is similar to my 2nd idea, _to serialize them_, but use `NSCondition` instead of `@synchronized`. Thank you for your answer. – 3329 May 28 '14 at 15:24