16

Apple's multithreading docs don't list NSIndexPath as threadsafe or not! As an immutable class, I'd generally expect it to be threadsafe.

Previously, I'm sure the documentation used to state that NSIndexPath instances were shared and globally unique. That seems to have disappeared now though, leading me to suspect that design was revised for iOS5 / Mac OS X 10.7.

I'm seeing quite a lot of crash reports from customers on Mac OS X 10.6 (Snow Leopard) which appear to be crashing trying to access an index path. Thus I wonder: are the actual instances thread safe, but that the logic for pulling them out of the shared cache isn't? Does anybody have any insight?

Here's an example stack trace BTW:

Dispatch queue: com.apple.root.default-priority
0 libobjc.A.dylib 0x96513f29 _cache_getImp + 9
1 libobjc.A.dylib 0x965158f0 class_respondsToSelector + 59
2 com.apple.CoreFoundation 0x948bcb49 ___forwarding___ + 761
3 com.apple.CoreFoundation 0x948bc7d2 _CF_forwarding_prep_0 + 50
4 com.apple.Foundation 0x994b10c5 -[NSIndexPath compare:] + 93
5 com.apple.Foundation 0x99415686 _NSCompareObject + 76
6 com.apple.CoreFoundation 0x948af61c __CFSimpleMergeSort + 236
7 com.apple.CoreFoundation 0x948af576 __CFSimpleMergeSort + 70
8 com.apple.CoreFoundation 0x948af38c CFSortIndexes + 252
9 com.apple.CoreFoundation 0x948fe80d CFMergeSortArray + 125
10 com.apple.Foundation 0x994153d3 _sortedObjectsUsingDescriptors + 639
11 com.apple.Foundation 0x994150d8 -[NSArray(NSKeyValueSorting) sortedArrayUsingDescriptors:] + 566

To me, that is an NSIndexPath instance trying to compare itself to a deallocated instance.

Mike Abdullah
  • 14,933
  • 2
  • 50
  • 75
  • 1
    What do you do with those index paths, and where does crash happen? Multithreading bugs are mysterious, a crash with an `NSIndexPath` does not necessarily mean the problem is in `NSIndexPath`. – hamstergene Feb 10 '12 at 13:24
  • I execute a fetch request and then sort the results based on their `-indexPath` method. Internally, each time it's called, that method creates an index path that represents the object's position in the tree. It is my suspicion that I'm being handed shared `NSIndexPath`s which are then being deallocated shortly after on another thread. – Mike Abdullah Feb 10 '12 at 13:30
  • where is the NSIndexPath originating? Is it a property of the fetched object? – Max MacLeod Feb 14 '12 at 11:11
  • I have an `-indexPath` method on my managed objects. That method calculates the path on-demand, creating it via a combination of `+indexPathWithIndex:` and `-indexPathByAddingIndex:` calls – Mike Abdullah Feb 14 '12 at 23:20
  • Have you had any problems (which cannot be satisfactorily solved and explained) before with indexPath on single-thread projects ? – Stanley Feb 15 '12 at 09:55
  • I have not. I've never seen crashes from `NSIndexPath` in the past – Mike Abdullah Feb 15 '12 at 21:32
  • 1
    Good to know that at least it should be safe to use NSIndexPath on single-thread projects. As I am thinking of using it myself. Otherwise a plain C integer array could take its (NSIndexPath) place. – Stanley Feb 17 '12 at 19:33

2 Answers2

4

So far the best answer I have is as I suspect:

As of OS X 10.7 and iOS 5, NSIndexPath is thread safe. Prior to that, instances are thread safe because they are immutable, but the shared retrieval of existing instances is not.

To my method which returns index paths on-demand, I did this:

- (NSIndexPath *)indexPath;
{
    NSIndexPath *result = … // create the path as appropriate

    return [[result retain] autorelease];
}

Since implementing that last line of code, we've had no more crash reports from index paths.

The index paths are created by -indexPathByAddingIndex: or +indexPathWithIndex:.

The results I am seeing make me pretty certain that (prior to 10.7/iOS5) these methods are returning an existing NSIndexPath instance. That instance is not retained by the current thread in any way though, so the thread which first created the instance (main in our case) is releasing the path (likely through popping the autorelease pool) and leaving our worker thread with a dangling pointer, which crashes when used, as seen in the question.

It's all a bit terrifying, because if my analysis is correct, the retain/autorelease dance I've added is simply replacing one race condition with another, less-likely one.

Prior to 10.7/iOS5, I can think of only one true workaround: Limit all creation of index paths to the main thread. That could be rather slow if such code gets called a lot, so could be improved — at the cost of memory — by maintaining some kind of instance cache of your own for background threads to use. If the cache is retaining a path, then you know it won't be deallocated by the main thread.

Mike Abdullah
  • 14,933
  • 2
  • 50
  • 75
1

Apple don't specifically list NSIndexPath as thread safe, but they do say that immutable classes are generally safe and mutable ones generally aren't. Since NSIndexPath is immutable it's safe to assume it's thread safe.

But "thread safe" doesn't mean that it can't cause crashes by being released by one thread before you use it on another though. Thread safe generally just means that its mutator methods contain locking to prevent glitches due to two threads setting properties concurrently (which is why classes without mutator methods are generally thread safe, although lazy getters and shared instances can also cause problems).

It sounds like your bug is more likely due to using an autorelease pool or some other mechanism that causes your object to be released at a time outside your control. You should probably ensure that any concurrently accessed objects are stored in properties of long-lived classes so that you can control their lifespan.

Creating an autoreleased object and accessing it from another thread after you've removed all strong references to it is a dangerous racing game that is likely to cause hard-to-trace crashes regardless of whether the object in question is "thread safe".

Nick Lockwood
  • 40,865
  • 11
  • 112
  • 103
  • Yes, I understand the variety of dangers in multi-threaded code. I am not creating these objects on one thread and expecting them to remain living on another thread. I am creating the objects by asking `NSIndexPath` for them on the worker thread. My understanding is that on 10.6 and earlier, `NSIndexPath` instances are globally shared. I suspect this global cache is not thread safe, rather than the individual instances. – Mike Abdullah Feb 22 '12 at 01:01
  • 1
    You can still read the iOS 4.3 / OS 10.6 docs in the Organiser in Xcode if you download the older doc sets and search all doc sets. I've found the line you're referring to: "NSIndexPath objects are uniqued and shared. If an index path containing the specified index or indexes already exists, that object is returned instead of a new instance." And yes, it does seem to have been removed in the latest docs, so perhaps you're right about NSIndexPath not being thread-safe under 10.6. – Nick Lockwood Feb 22 '12 at 08:00