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.h | 3 +- Foundation/GTMNSThread+Blocks.m | 211 +++++++++++++++++++++++----- Foundation/GTMNSThread+BlocksTest.m | 267 ++++++++++++++++++++++++++++-------- 3 files changed, 391 insertions(+), 90 deletions(-) (limited to 'Foundation') diff --git a/Foundation/GTMNSThread+Blocks.h b/Foundation/GTMNSThread+Blocks.h index b5df094..23e4e60 100644 --- a/Foundation/GTMNSThread+Blocks.h +++ b/Foundation/GTMNSThread+Blocks.h @@ -50,8 +50,9 @@ NSConditionLock *runLock_; } -// Will stop the thread. +// Will stop the thread, blocking till the thread exits. - (void)stop; + @end #endif // GTM_IPHONE_SDK || (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5) 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) diff --git a/Foundation/GTMNSThread+BlocksTest.m b/Foundation/GTMNSThread+BlocksTest.m index e900b5b..606de6d 100644 --- a/Foundation/GTMNSThread+BlocksTest.m +++ b/Foundation/GTMNSThread+BlocksTest.m @@ -16,6 +16,7 @@ // under the License. // +#import #import "GTMSenTestCase.h" #import "GTMNSThread+Blocks.h" @@ -25,90 +26,240 @@ @interface GTMNSThread_BlocksTest : GTMTestCase { @private - NSThread *workerThread_; - BOOL workerRunning_; + GTMSimpleWorkerThread *workerThread_; } - -@property (nonatomic, readwrite, getter=isWorkerRunning) BOOL workerRunning; @end @implementation GTMNSThread_BlocksTest -@synthesize workerRunning = workerRunning_; - -- (void)stopTestRunning:(GTMUnitTestingBooleanRunLoopContext *)context{ - [context setShouldStop:YES]; +- (void)setUp { + workerThread_ = [[GTMSimpleWorkerThread alloc] init]; + [workerThread_ start]; } -- (void)workerMain:(id)object { - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - while ([self isWorkerRunning]) { - [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode - beforeDate:[NSDate distantFuture]]; - } - [pool drain]; +- (void)tearDown { + [workerThread_ stop]; + [workerThread_ release]; } -- (void)killWorkerThread:(GTMUnitTestingBooleanRunLoopContext *)context { - [self setWorkerRunning:NO]; - [context setShouldStop:YES]; +- (void)testPerformBlockOnCurrentThread { + NSThread *currentThread = [NSThread currentThread]; + + GTMUnitTestingBooleanRunLoopContext *context = + [GTMUnitTestingBooleanRunLoopContext context]; + __block NSThread *runThread = nil; + + // Straight block runs right away (no runloop spin) + runThread = nil; + [context setShouldStop:NO]; + [currentThread gtm_performBlock:^{ + runThread = [NSThread currentThread]; + [context setShouldStop:YES]; + }]; + STAssertEqualObjects(runThread, currentThread, nil); + STAssertTrue([context shouldStop], nil); + + // Block with waiting runs immediately as well. + runThread = nil; + [context setShouldStop:NO]; + [currentThread gtm_performWaitingUntilDone:YES block:^{ + runThread = [NSThread currentThread]; + [context setShouldStop:YES]; + }]; + STAssertEqualObjects(runThread, currentThread, nil); + STAssertTrue([context shouldStop], nil); + + // Block without waiting requires a runloop spin. + runThread = nil; + [context setShouldStop:NO]; + [currentThread gtm_performWaitingUntilDone:NO block:^{ + runThread = [NSThread currentThread]; + [context setShouldStop:YES]; + }]; + STAssertTrue([[NSRunLoop currentRunLoop] + gtm_runUpToSixtySecondsWithContext:context], nil); + STAssertEqualObjects(runThread, currentThread, nil); + STAssertTrue([context shouldStop], nil); } -- (void)setUp { - [self setWorkerRunning:YES]; - workerThread_ = [[NSThread alloc] initWithTarget:self - selector:@selector(workerMain:) - object:nil]; - [workerThread_ start]; +- (void)testPerformBlockInBackground { + GTMUnitTestingBooleanRunLoopContext *context = + [GTMUnitTestingBooleanRunLoopContext context]; + __block NSThread *runThread = nil; + [NSThread gtm_performBlockInBackground:^{ + runThread = [NSThread currentThread]; + [context setShouldStop:YES]; + }]; + STAssertTrue([[NSRunLoop currentRunLoop] + gtm_runUpToSixtySecondsWithContext:context], nil); + STAssertNotNil(runThread, nil); + STAssertNotEqualObjects(runThread, [NSThread currentThread], nil); } -- (void)tearDown { - GTMUnitTestingBooleanRunLoopContext *context - = [GTMUnitTestingBooleanRunLoopContext context]; - [self performSelector:@selector(killWorkerThread:) - onThread:workerThread_ - withObject:context - waitUntilDone:NO]; - NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; - STAssertTrue([runLoop gtm_runUpToSixtySecondsWithContext:context], nil); - [workerThread_ release]; +- (void)testWorkerThreadBasics { + // Unstarted worker isn't running. + GTMSimpleWorkerThread *worker = [[GTMSimpleWorkerThread alloc] init]; + STAssertFalse([worker isExecuting], nil); + STAssertFalse([worker isFinished], nil); + + // Unstarted worker can be stopped without error. + [worker stop]; + STAssertFalse([worker isExecuting], nil); + STAssertTrue([worker isFinished], nil); + + // And can be stopped again + [worker stop]; + STAssertFalse([worker isExecuting], nil); + STAssertTrue([worker isFinished], nil); + + // A thread we start can be stopped with correct state. + worker = [[GTMSimpleWorkerThread alloc] init]; + STAssertFalse([worker isExecuting], nil); + STAssertFalse([worker isFinished], nil); + [worker start]; + STAssertTrue([worker isExecuting], nil); + STAssertFalse([worker isFinished], nil); + [worker stop]; + STAssertFalse([worker isExecuting], nil); + STAssertTrue([worker isFinished], nil); + + // A cancel is also honored + worker = [[GTMSimpleWorkerThread alloc] init]; + STAssertFalse([worker isExecuting], nil); + STAssertFalse([worker isFinished], nil); + [worker start]; + STAssertTrue([worker isExecuting], nil); + STAssertFalse([worker isFinished], nil); + [worker cancel]; + // And after some time we're done. We're generous here, this needs to + // exceed the worker thread's runloop timeout. + sleep(5); + STAssertFalse([worker isExecuting], nil); + STAssertTrue([worker isFinished], nil); } -- (void)testPerformBlock { - NSThread *currentThread = [NSThread currentThread]; - GTMUnitTestingBooleanRunLoopContext *context - = [GTMUnitTestingBooleanRunLoopContext context]; +- (void)testWorkerThreadStopTiming { + // Throw a sleep and make sure that we stop as soon as we can. + NSDate *start = [NSDate date]; + NSConditionLock *threadLock = [[[NSConditionLock alloc] initWithCondition:0] + autorelease]; [workerThread_ gtm_performBlock:^{ - [self performSelector:@selector(stopTestRunning:) - onThread:currentThread - withObject:context - waitUntilDone:YES]; + [threadLock lock]; + [threadLock unlockWithCondition:1]; + sleep(10); }]; - NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; - STAssertTrue([runLoop gtm_runUpToSixtySecondsWithContext:context], nil); + [threadLock lockWhenCondition:1]; + [threadLock unlock]; + [workerThread_ stop]; + STAssertFalse([workerThread_ isExecuting], nil); + STAssertTrue([workerThread_ isFinished], nil); + STAssertEqualsWithAccuracy(-[start timeIntervalSinceNow], 10.0, 2.0, nil); } -- (void)testPerformBlockWaitUntilDone { - GTMUnitTestingBooleanRunLoopContext *context - = [GTMUnitTestingBooleanRunLoopContext context]; +- (void)testPerformBlockOnWorkerThread { + GTMUnitTestingBooleanRunLoopContext *context = + [GTMUnitTestingBooleanRunLoopContext context]; + __block NSThread *runThread = nil; + + // Runs on the other thread + runThread = nil; + [context setShouldStop:NO]; + [workerThread_ gtm_performBlock:^{ + runThread = [NSThread currentThread]; + [context setShouldStop:YES]; + }]; + STAssertTrue([[NSRunLoop currentRunLoop] + gtm_runUpToSixtySecondsWithContext:context], nil); + STAssertNotNil(runThread, nil); + STAssertEqualObjects(runThread, workerThread_, nil); + + // Other thread no wait. + runThread = nil; + [context setShouldStop:NO]; + [workerThread_ gtm_performWaitingUntilDone:NO block:^{ + runThread = [NSThread currentThread]; + [context setShouldStop:YES]; + }]; + STAssertTrue([[NSRunLoop currentRunLoop] + gtm_runUpToSixtySecondsWithContext:context], nil); + STAssertNotNil(runThread, nil); + STAssertEqualObjects(runThread, workerThread_, nil); + + // Waiting requires no runloop spin + runThread = nil; + [context setShouldStop:NO]; [workerThread_ gtm_performWaitingUntilDone:YES block:^{ + runThread = [NSThread currentThread]; [context setShouldStop:YES]; }]; STAssertTrue([context shouldStop], nil); + STAssertNotNil(runThread, nil); + STAssertEqualObjects(runThread, workerThread_, nil); } -- (void)testPerformBlockInBackground { - NSThread *currentThread = [NSThread currentThread]; - GTMUnitTestingBooleanRunLoopContext *context - = [GTMUnitTestingBooleanRunLoopContext context]; - [NSThread gtm_performBlockInBackground:^{ - [self performSelector:@selector(stopTestRunning:) - onThread:currentThread - withObject:context - waitUntilDone:YES]; +- (void)testExitingBlockIsExecuting { + NSConditionLock *threadLock = [[[NSConditionLock alloc] initWithCondition:0] + autorelease]; + [workerThread_ gtm_performWaitingUntilDone:NO block:^{ + [threadLock lock]; + [threadLock unlockWithCondition:1]; + pthread_exit(NULL); + }]; + [threadLock lockWhenCondition:1]; + [threadLock unlock]; + // Give the pthread_exit() a bit of time + sleep(5); + // Did we notice the thread died? Does [... isExecuting] clean up? + STAssertFalse([workerThread_ isExecuting], nil); + STAssertTrue([workerThread_ isFinished], nil); +} + +- (void)testExitingBlockCancel { + NSConditionLock *threadLock = [[[NSConditionLock alloc] initWithCondition:0] + autorelease]; + [workerThread_ gtm_performWaitingUntilDone:NO block:^{ + [threadLock lock]; + [threadLock unlockWithCondition:1]; + pthread_exit(NULL); + }]; + [threadLock lockWhenCondition:1]; + [threadLock unlock]; + // Give the pthread_exit() a bit of time + sleep(5); + // Cancel/stop the thread + [workerThread_ stop]; + // Did we notice the thread died? Did we clean up? + STAssertFalse([workerThread_ isExecuting], nil); + STAssertTrue([workerThread_ isFinished], nil); +} + +- (void)testStopFromThread { + NSConditionLock *threadLock = [[[NSConditionLock alloc] initWithCondition:0] + autorelease]; + [workerThread_ gtm_performWaitingUntilDone:NO block:^{ + [threadLock lock]; + [workerThread_ stop]; // Shold not block. + [threadLock unlockWithCondition:1]; + }]; + // Block should complete before the stop occurs. + [threadLock lockWhenCondition:1]; + [threadLock unlock]; + // Still need to give the thread a moment to not be executing + sleep(5); + STAssertFalse([workerThread_ isExecuting], nil); + STAssertTrue([workerThread_ isFinished], nil); +} + +- (void)testPThreadName { + NSString *testName = @"InigoMontoya"; + [workerThread_ setName:testName]; + [workerThread_ gtm_performWaitingUntilDone:NO block:^{ + STAssertEqualObjects([workerThread_ name], testName, nil); + char threadName[100]; + pthread_getname_np(pthread_self(), threadName, 100); + STAssertEqualObjects([NSString stringWithUTF8String:threadName], + testName, nil); }]; - NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; - STAssertTrue([runLoop gtm_runUpToSixtySecondsWithContext:context], nil); } @end -- cgit v1.2.3