A: 

This is as close to a wild guess as I'm ever going to give on stack overflow, but here goes: I think that -dealloc is synchronized to the same lock as -retain and -release, which would be crazy to not be atomic. This lock is not gotten magically in dealloc, as clearly that is filled with you're own code, but rather in release, it just holds the same lock while it does its dealloc. (this may be one of the reason's you're not supposed to call dealloc directly)

Now in object B, [self delegate] calls object A's retain, which is, if I'm right, atomic in respect to dealloc and release, and will either occur before -[A dealloc] because it will occur before -[A release], or will happen after -[A dealloc], depending on its timing.

In the first case, where -[A retain] happens before -[A release], the outcome is obvious: object A will not be deallocated until the following -[A autorelease] from the same accessor, and object B will call the delegate method on the still-around object A.

The second case is much trickier, and from this point forth, we shall be leaving the firm foundation of fact and journeying together through the murky marshes of memory into thickets of wildest guesswork. I believe that in the second case, -[A dealloc] attempts to set the delegate of object B (as said before, while the other thread is waiting to acquire the lock on its delegate) to nil. However, with an atomic property, A would then have to acquire the lock B had acquired and was using while waiting for the lock A used for retain/release/dealloc, which is obviously in use.

I think therefore this would cause a deadlock, although again, I am entirely unsure, and this answer is largely based on guesses about what is locked and when. Once again, my only viable solution (but don't stop looking, there must be a better way) is to retain the delegate, at least while the second thread is running, and prevent it from being deallocated in the first place.

Jared P
Thanks for your answer Jared. I agree that retaining the delegate the whole time the second thread is active would solve it, but this specifically seems to go against the advice in the Cocoa fundamentals guide, which states "Delegating objects do not (and should not) retain their delegates". As you say, I'll keep looking. I'll report back if/when I find another solution.
JosephH
Don't know if this is too late or not, but I just had another idea for how to fix this: rather than call -[delegate didFinish:] on the main thread, you could instead call something like [self alertDelegateDidFinish] on the main thread, the contents of which would be [self.delegate didFinish:self]
Jared P
Thanks - that's quite close to what I did end up doing - I'm about to update my original question ( http://stackoverflow.com/questions/3158356/how-to-handle-setdelegate-when-using-multipe-threads ) with the final method I used, which works well works.
JosephH
+1  A: 

I don't think there's a lock on dealloc versus retain/release. The following example has a dealloc method with a sleep() in it (does anyone know if sleep() breaks locks? I don't think it does, but you never know). A better example might be to repeatedly instantiate/destroy instances of A and B until you get a situation like the one mentioned here, without the sleep().

View controller, in my case, but could be anything:

-(void)septhreadRetainDel
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSLog(@"[thread#2] sleep(1.f);");
    sleep(1.f);
    NSLog(@"[thread#2] [b retainDelegate];");
    [b retainDelegate];
    NSLog(@"[thread#2] sleep(2.f);");
    sleep(2.f);
    NSLog(@"[thread#2] [b release];");
    [b release];
    [pool release];
}

- (void)viewDidLoad {
    NSLog(@"-(void)viewDidLoad:");
    [super viewDidLoad];
    NSLog(@"a = [[A alloc] init];");
    a = [[A alloc] init];
    NSLog(@"[a autorelease];");
    [a autorelease];
    NSLog(@"b = [[B alloc] init];");
    b = [[B alloc] init];
    NSLog(@"b.delegate = a;");
    b.delegate = a;
    NSLog(@"[NSThread detachNewThreadSelector:@selector(septhreadRetainDel) toTarget:self withObject:nil];");
    [NSThread detachNewThreadSelector:@selector(septhreadRetainDel) toTarget:self withObject:nil];
}

A:

#import "A.h"

@implementation A

-(void)dealloc
{
    NSLog(@"A: dealloc; zzz for 2s");
    sleep(2.f);
    NSLog(@"A: dealloc; waking up in time for my demise!");
    [super dealloc];
}
-(id)retain
{
    NSLog(@"A retain (%d++>%d)", self.retainCount, self.retainCount+1);
    return [super retain];
}
-(void)release
{
    NSLog(@"A release (%d-->%d)", self.retainCount, self.retainCount-1);
    [super release];
}

@end

B (.h):

#import "A.h"

@interface B : NSObject {
    A *delegate;
}

-(void) retainDelegate;

@property (nonatomic, assign) A *delegate;

@end

B (.m):

#import "B.h"

@implementation B

@synthesize delegate;

-(void)retainDelegate
{
    NSLog(@"B:: -(void)retainDelegate (delegate currently has %d retain count):", delegate.retainCount);
    NSLog(@"B:: [delegate retain];");
    [delegate retain];
}
-(void)releaseDelegate
{
    NSLog(@"B releases delegate");
    [delegate release];
    delegate = nil;
}

-(void)dealloc
{
    NSLog(@"B dealloc; closing shop");
    [self releaseDelegate];
    [super dealloc];
}

-(id)retain
{
    NSLog(@"B retain (%d++>%d)", self.retainCount, self.retainCount+1);
    return [super retain];
}
-(void)release
{
    NSLog(@"B release (%d-->%d)", self.retainCount, self.retainCount-1);
    [super release];    
}

@end

The program ends up crashing with EXC_BAD_ACCESS at B's releaseDelegate method. The following is the output from the NSLogs:

2010-07-10 11:49:27.044 race[832:207] -(void)viewDidLoad:
2010-07-10 11:49:27.050 race[832:207] a = [[A alloc] init];
2010-07-10 11:49:27.053 race[832:207] [a autorelease];
2010-07-10 11:49:27.056 race[832:207] b = [[B alloc] init];
2010-07-10 11:49:27.058 race[832:207] b.delegate = a;
2010-07-10 11:49:27.061 race[832:207] [NSThread detachNewThreadSelector:@selector(septhreadRetainDel) toTarget:self withObject:nil];
2010-07-10 11:49:27.064 race[832:4703] [thread#2] sleep(1.f);
2010-07-10 11:49:27.082 race[832:207] A release (1-->0)
2010-07-10 11:49:27.089 race[832:207] A: dealloc; zzz for 2s
2010-07-10 11:49:28.066 race[832:4703] [thread#2] [b retainDelegate];
2010-07-10 11:49:28.072 race[832:4703] B:: -(void)retainDelegate (delegate currently has 1 retain count):
2010-07-10 11:49:28.076 race[832:4703] B:: [delegate retain];
2010-07-10 11:49:28.079 race[832:4703] A retain (1++>2)
2010-07-10 11:49:28.081 race[832:4703] [thread#2] sleep(2.f);
2010-07-10 11:49:29.092 race[832:207] A: dealloc; waking up in time for my demise!
2010-07-10 11:49:30.084 race[832:4703] [thread#2] [b release];
2010-07-10 11:49:30.089 race[832:4703] B release (1-->0)
2010-07-10 11:49:30.094 race[832:4703] B dealloc; closing shop
2010-07-10 11:49:30.097 race[832:4703] B releases delegate
Program received signal:  “EXC_BAD_ACCESS”.

Once -dealloc is called, retain counts are no longer of import. The object will be destroyed (this is probably obvious, though I wonder what would happen if you checked self's retainCount and DID NOT call [super dealloc] if the object had retains... insane idea). Now if we modify the -dealloc for A to set B's delegate to nil first, the program works but only because we're nil'ing delegate in B in releaseDelegate.

I don't know if that answers your question, really, but presuming sleep()'s are not somehow breaking thread locks, the exact same behavior should happen when dealloc is called right before a retain.

XCode project is available here in case you want to play with it: http://www.megaupload.com/?d=P02152EM

Kalle
I think you've conclusively proved it is a race condition! Very nice work.
JosephH