aboutsummaryrefslogtreecommitdiff
path: root/Foundation
diff options
context:
space:
mode:
authorGravatar gtm.daemon <gtm.daemon@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2012-12-18 00:30:10 +0000
committerGravatar gtm.daemon <gtm.daemon@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2012-12-18 00:30:10 +0000
commita18f91633f5bc0d805f167c340832b4515f5d682 (patch)
tree92cae0424db673064daad791a2ac92c5c55af006 /Foundation
parente352172c303e12a7807d779042a74d314f76e277 (diff)
[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)
Diffstat (limited to 'Foundation')
-rw-r--r--Foundation/GTMNSThread+Blocks.h3
-rw-r--r--Foundation/GTMNSThread+Blocks.m211
-rw-r--r--Foundation/GTMNSThread+BlocksTest.m267
3 files changed, 391 insertions, 90 deletions
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 <dlfcn.h>
// 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 <pthread.h>
#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