From a18f91633f5bc0d805f167c340832b4515f5d682 Mon Sep 17 00:00:00 2001 From: "gtm.daemon" Date: Tue, 18 Dec 2012 00:30:10 +0000 Subject: [Author: aharper] Improve worker thread implementation: - Handle more of the NSThread interface. - Allow cancel or stop before we've started. - Cache looked up pthread_setname_np symbol in thread local storage. R=dmaclach,thomasvl APPROVED=dmaclach DELTA=441 (339 added, 38 deleted, 64 changed) --- Foundation/GTMNSThread+Blocks.m | 211 ++++++++++++++++++++++++++++++++++------ 1 file changed, 180 insertions(+), 31 deletions(-) (limited to 'Foundation/GTMNSThread+Blocks.m') diff --git a/Foundation/GTMNSThread+Blocks.m b/Foundation/GTMNSThread+Blocks.m index f835a0b..7b38f61 100644 --- a/Foundation/GTMNSThread+Blocks.m +++ b/Foundation/GTMNSThread+Blocks.m @@ -22,7 +22,8 @@ #import // Only available 10.6 and later. -typedef int (*pthread_setname_np_Ptr)(const char*); +typedef int (*PThreadSetNameNPPTr)(const char*); +static PThreadSetNameNPPTr gPThreadSetNameNP = NULL; #if NS_BLOCKS_AVAILABLE @@ -59,63 +60,163 @@ typedef int (*pthread_setname_np_Ptr)(const char*); #if GTM_IPHONE_SDK || (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5) enum { - GTMSimpleThreadIsStarting = 1, - GTMSimpleThreadIsFinished + kGTMSimpleThreadInitialized = 0, + kGTMSimpleThreadStarting, + kGTMSimpleThreadRunning, + kGTMSimpleThreadCancel, + kGTMSimpleThreadFinished, }; @implementation GTMSimpleWorkerThread ++ (void)initialize { + if ([self class] == [GTMSimpleWorkerThread class]) { + // Resolve pthread_setname_np() on 10.6 and later. + gPThreadSetNameNP = dlsym(RTLD_DEFAULT, "pthread_setname_np"); + } +} + - (id)init { if ((self = [super init])) { runLock_ = - [[NSConditionLock alloc] initWithCondition:GTMSimpleThreadIsStarting]; + [[NSConditionLock alloc] initWithCondition:kGTMSimpleThreadInitialized]; } return self; } - (void)dealloc { + if ([self isExecuting]) { + [self stop]; + } [runLock_ release]; [super dealloc]; } - (void)setThreadDebuggerName:(NSString *)name { - // [NSThread setName:] doesn't actually set the name in such a way that the - // debugger can see it. So we handle it here instead. - // pthread_setname_np only available 10.6 and later, look up dynamically. - pthread_setname_np_Ptr setName = dlsym(RTLD_DEFAULT, "pthread_setname_np"); - if (!setName) return; - setName([name UTF8String]); + if (gPThreadSetNameNP) gPThreadSetNameNP([name UTF8String]); } - (void)main { - _GTMDevAssert([runLock_ condition] == GTMSimpleThreadIsStarting, - @"Bad start condition %d", [runLock_ condition]); - [runLock_ lockWhenCondition:GTMSimpleThreadIsStarting]; - - [self setThreadDebuggerName:[self name]]; + [runLock_ lock]; + if ([runLock_ condition] != kGTMSimpleThreadStarting) { + // Don't start, we're already cancelled or we've been started twice. + [runLock_ unlock]; + return; + } + + // Give ourself an autopool + NSAutoreleasePool *localPool = [[NSAutoreleasePool alloc] init]; + + // Expose the current runloop so other threads can stop (but see caveat + // below). + NSRunLoop *loop = [NSRunLoop currentRunLoop]; + runLoop_ = [loop getCFRunLoop]; + if (runLoop_) CFRetain(runLoop_); // NULL check is pure paranoia. // Add a port to the runloop so that it stays alive. Without a port attached // to it, a runloop will immediately return when you call run on it. - NSPort *tempPort = [NSPort port]; - NSRunLoop *loop = [NSRunLoop currentRunLoop]; - [loop addPort:tempPort forMode:NSDefaultRunLoopMode]; + [loop addPort:[NSPort port] forMode:NSDefaultRunLoopMode]; - // Run the loop using CFRunLoopRun because [NSRunLoop run] will sometimes nest - // runloops making it impossible to stop. - runLoop_ = [loop getCFRunLoop]; - CFRunLoopRun(); - [runLock_ unlockWithCondition:GTMSimpleThreadIsFinished]; + // Name ourself + [self setThreadDebuggerName:[self name]]; + + // We're officially running. + [runLock_ unlockWithCondition:kGTMSimpleThreadRunning]; + + while (![self isCancelled] && + [runLock_ tryLockWhenCondition:kGTMSimpleThreadRunning]) { + [runLock_ unlock]; + // We can't control nesting of runloops, so we spin with a short timeout. If + // another thread cancels us the CFRunloopStop() we might get it right away, + // if there is no nesting, otherwise our timeout will still get us to exit + // in reasonable time. + [loop runMode:NSDefaultRunLoopMode + beforeDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; + [localPool drain]; + localPool = [[NSAutoreleasePool alloc] init]; + } + + // Exit + [runLock_ lock]; + [localPool drain]; + if (runLoop_) CFRelease(runLoop_); + runLoop_ = NULL; + [runLock_ unlockWithCondition:kGTMSimpleThreadFinished]; +} + +- (void)start { + // Before we start the thread we need to make sure its not already running + // and that the lock is past kGTMSimpleThreadInitialized so an immediate + // stop is safe. + [runLock_ lock]; + if ([runLock_ condition] != kGTMSimpleThreadInitialized) { + [runLock_ unlock]; + return; + } + [runLock_ unlockWithCondition:kGTMSimpleThreadStarting]; + [super start]; +} + +- (void)cancel { + // NSThread appears to not propagate [... isCancelled] to our thread in + // this subclass, so we'll let super know and then use our condition lock. + [super cancel]; + [runLock_ lock]; + switch ([runLock_ condition]) { + case kGTMSimpleThreadInitialized: + case kGTMSimpleThreadStarting: + // Cancelled before we started? Transition straight to finished. + [runLock_ unlockWithCondition:kGTMSimpleThreadFinished]; + return; + case kGTMSimpleThreadRunning: + // If the thread has exited without changing lock state we detect that + // here. Note this is a direct call to [super isExecuting] to prevent + // deadlock on |runLock_| in [self isExecuting]. + if (![super isExecuting]) { + // Thread died in some unanticipated way, clean up on its behalf. + if (runLoop_) { + CFRelease(runLoop_); + runLoop_ = NULL; + } + [runLock_ unlockWithCondition:kGTMSimpleThreadFinished]; + return; + } else { + // We need to cancel the running loop. We'd like to stop the runloop + // right now if we can (but see the caveat above about nested runloops). + if (runLoop_) CFRunLoopStop(runLoop_); + [runLock_ unlockWithCondition:kGTMSimpleThreadCancel]; + return; + } + case kGTMSimpleThreadCancel: + case kGTMSimpleThreadFinished: + // Already cancelled or finished. There's an outside chance the thread + // will have died now (imagine a [... dealloc] that calls pthread_exit()) + // but we'll ignore those cases. + [runLock_ unlock]; + return; + } } - (void)stop { - CFRunLoopStop(runLoop_); - if (![[NSThread currentThread] isEqual:self]) { - // If we are calling stop from a separate thread, we block until the - // simple thread actually leaves the runloop so there is no race condition - // between the current thread and the simple thread. In effect it's a - // join operation. - [runLock_ lockWhenCondition:GTMSimpleThreadIsFinished]; - [runLock_ unlockWithCondition:GTMSimpleThreadIsFinished]; + // Cancel does the heavy lifting... + [self cancel]; + + // If we're the current thread then the stop was called from within our + // own runloop and we need to return control now. [... main] will handle + // the shutdown on its own. + if ([[NSThread currentThread] isEqual:self]) return; + + // From all other threads block till we're finished. Note that [... cancel] + // handles ensuring we will either already be in this state or transition + // there after thread exit. + [runLock_ lockWhenCondition:kGTMSimpleThreadFinished]; + [runLock_ unlock]; + + // We could still be waiting for thread teardown at this point (lock is in + // the right state, but thread is not quite torn down), so spin till we say + // execution is complete (our implementation checks superclass). + while ([self isExecuting]) { + usleep(10); } } @@ -129,6 +230,54 @@ enum { [super setName:name]; } +- (BOOL)isCancelled { + if ([super isCancelled]) return YES; + BOOL cancelled = NO; + [runLock_ lock]; + if ([runLock_ condition] == kGTMSimpleThreadCancel) { + cancelled = YES; + } + [runLock_ unlock]; + return cancelled; +} + +- (BOOL)isExecuting { + if ([super isExecuting]) return YES; + [runLock_ lock]; + switch ([runLock_ condition]) { + case kGTMSimpleThreadStarting: + // While starting we may not be executing yet, but we'll pretend we are. + [runLock_ unlock]; + return YES; + case kGTMSimpleThreadCancel: + case kGTMSimpleThreadRunning: + // Both of these imply we're running, but [super isExecuting] failed, + // so the thread died for other reasons. Clean up. + if (runLoop_) { + CFRelease(runLoop_); + runLoop_ = NULL; + } + [runLock_ unlockWithCondition:kGTMSimpleThreadFinished]; + break; + default: + [runLock_ unlock]; + break; + } + return NO; +} + +- (BOOL)isFinished { + if ([super isFinished]) return YES; + if ([self isExecuting]) return NO; // Will clean up dead thread. + BOOL finished = NO; + [runLock_ lock]; + if ([runLock_ condition] == kGTMSimpleThreadFinished) { + finished = YES; + } + [runLock_ unlock]; + return finished; +} + @end #endif // GTM_IPHONE_SDK || (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5) -- cgit v1.2.3