aboutsummaryrefslogtreecommitdiff
path: root/Foundation
diff options
context:
space:
mode:
authorGravatar Dave MacLachlan <dmaclach@gmail.com>2018-11-08 12:37:58 -0800
committerGravatar Thomas Van Lenten <thomasvl@google.com>2018-11-08 15:47:58 -0500
commit585330a68d00c4d76927ff7bf4829471944358ab (patch)
tree6e178f5b4f10d8fb8e5745750f1603a4446183e7 /Foundation
parent01b882dad6f7668ed0387147952b63399f97cf19 (diff)
Fix up a race condition in GTMSimpleWorkerThread
Basically a complete rewrite of GTMSimpleWorkerThread that should get rid of a race condition when stopping tests and testing for "isExecuting/isFinished". It does change the observed behaviour of GTMSimpleWorkerThread a little in that "start" no longer blocks until the thread is starting, and we removed the unused "stop" method.
Diffstat (limited to 'Foundation')
-rw-r--r--Foundation/GTMNSThread+Blocks.h10
-rw-r--r--Foundation/GTMNSThread+Blocks.m246
-rw-r--r--Foundation/GTMNSThread+BlocksTest.m168
3 files changed, 105 insertions, 319 deletions
diff --git a/Foundation/GTMNSThread+Blocks.h b/Foundation/GTMNSThread+Blocks.h
index 17bfbc7..4240b4c 100644
--- a/Foundation/GTMNSThread+Blocks.h
+++ b/Foundation/GTMNSThread+Blocks.h
@@ -40,13 +40,5 @@
// A simple thread that does nothing but handle performBlock and
// performSelector calls.
-@interface GTMSimpleWorkerThread : NSThread {
- @private
- CFRunLoopRef runLoop_;
- NSConditionLock *runLock_;
-}
-
-// Will stop the thread, blocking till the thread exits.
-- (void)stop;
-
+@interface GTMSimpleWorkerThread : NSThread
@end
diff --git a/Foundation/GTMNSThread+Blocks.m b/Foundation/GTMNSThread+Blocks.m
index 8318193..e754f6f 100644
--- a/Foundation/GTMNSThread+Blocks.m
+++ b/Foundation/GTMNSThread+Blocks.m
@@ -53,220 +53,76 @@
#endif // NS_BLOCKS_AVAILABLE
-enum {
- kGTMSimpleThreadInitialized = 0,
- kGTMSimpleThreadStarting,
- kGTMSimpleThreadRunning,
- kGTMSimpleThreadCancel,
- kGTMSimpleThreadFinished,
-};
-
-@implementation GTMSimpleWorkerThread
-
-- (id)init {
- if ((self = [super init])) {
- runLock_ =
- [[NSConditionLock alloc] initWithCondition:kGTMSimpleThreadInitialized];
- }
- return self;
+@implementation GTMSimpleWorkerThread {
+ NSLock *sourceLock_;
+ CFRunLoopSourceRef source_; // Protected by sourceLock_
+ CFRunLoopRef cfRunLoop_;
}
-- (void)dealloc {
- if ([self isExecuting]) {
- [self stop];
- }
- [runLock_ release];
- [super dealloc];
+static void RunLoopContextEmptyFunc(void *info) {
+ // Empty because the source is used solely for signalling.
+ // The documentation for CFRunLoopSourceContext does not
+ // make it clear if you can have a null perform method.
}
-- (void)setThreadDebuggerName:(NSString *)name {
- if ([name length]) {
- pthread_setname_np([name UTF8String]);
- } else {
- pthread_setname_np("");
+- (void)main {
+ NSRunLoop *nsRunLoop = [NSRunLoop currentRunLoop];
+ { // Braces are just to denote what is protected by sourceLock_
+ [sourceLock_ lock];
+ cfRunLoop_ = [nsRunLoop getCFRunLoop];
+ CFRetain(cfRunLoop_);
+ CFRunLoopSourceContext context = {0};
+ context.perform = RunLoopContextEmptyFunc;
+ source_ = CFRunLoopSourceCreate(NULL, 0, &context);
+ CFRunLoopAddSource(cfRunLoop_, source_, kCFRunLoopCommonModes);
+ [sourceLock_ unlock];
+ }
+ while (true) {
+ BOOL cancelled = [self isCancelled];
+ if (cancelled) {
+ break;
+ }
+ BOOL ranLoop = [nsRunLoop runMode:NSDefaultRunLoopMode
+ beforeDate:[NSDate distantFuture]];
+ if (!ranLoop) {
+ break;
+ }
}
}
-- (void)main {
- [runLock_ lock];
- if ([runLock_ condition] != kGTMSimpleThreadStarting) {
- // Don't start, we're already cancelled or we've been started twice.
- [runLock_ unlock];
- return;
+- (void)dealloc {
+ if (cfRunLoop_) {
+ CFRelease(cfRunLoop_);
}
-
- // 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.
- [loop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
-
- // 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];
+ if (source_) {
+ CFRelease(source_);
}
-
- // Exit
- [runLock_ lock];
- [localPool drain];
- if (runLoop_) CFRelease(runLoop_);
- runLoop_ = NULL;
- [runLock_ unlockWithCondition:kGTMSimpleThreadFinished];
+ [super dealloc];
}
- (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;
+ // Protect lock in case we are "started" twice in different threads.
+ // NSThread has no documentation regarding the safety of this, so
+ // making safe by default.
+ @synchronized (self) {
+ if (sourceLock_) {
+ return;
+ }
+ sourceLock_ = [[NSLock alloc] init];
}
- [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 {
- // 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);
- }
-}
-
-- (void)setName:(NSString *)name {
- if ([self isExecuting]) {
- [self performSelector:@selector(setThreadDebuggerName:)
- onThread:self
- withObject:name
- waitUntilDone:YES];
- }
- [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;
+ { // Braces are just to denote what is protected by sourceLock_
+ [sourceLock_ lock];
+ if (source_) {
+ CFRunLoopSourceSignal(source_);
+ CFRunLoopWakeUp(cfRunLoop_);
+ }
+ [sourceLock_ unlock];
}
- [runLock_ unlock];
- return finished;
}
@end
diff --git a/Foundation/GTMNSThread+BlocksTest.m b/Foundation/GTMNSThread+BlocksTest.m
index 4b685fd..ff1a1a0 100644
--- a/Foundation/GTMNSThread+BlocksTest.m
+++ b/Foundation/GTMNSThread+BlocksTest.m
@@ -20,7 +20,7 @@
#import "GTMSenTestCase.h"
#import "GTMNSThread+Blocks.h"
-#import "GTMFoundationUnitTestingUtilities.h"
+static const NSTimeInterval kTestTimeout = 5;
@interface GTMNSThread_BlocksTest : GTMTestCase {
@private
@@ -36,60 +36,46 @@
}
- (void)tearDown {
- [workerThread_ stop];
+ [workerThread_ cancel];
[workerThread_ release];
}
- (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];
}];
XCTAssertEqualObjects(runThread, currentThread);
- XCTAssertTrue([context shouldStop]);
// Block with waiting runs immediately as well.
runThread = nil;
- [context setShouldStop:NO];
[currentThread gtm_performWaitingUntilDone:YES block:^{
runThread = [NSThread currentThread];
- [context setShouldStop:YES];
}];
XCTAssertEqualObjects(runThread, currentThread);
- XCTAssertTrue([context shouldStop]);
// Block without waiting requires a runloop spin.
runThread = nil;
- [context setShouldStop:NO];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"BlockRan"];
[currentThread gtm_performWaitingUntilDone:NO block:^{
runThread = [NSThread currentThread];
- [context setShouldStop:YES];
+ [expectation fulfill];
}];
- XCTAssertTrue([[NSRunLoop currentRunLoop]
- gtm_runUpToSixtySecondsWithContext:context]);
+ [self waitForExpectationsWithTimeout:kTestTimeout handler:NULL];
XCTAssertEqualObjects(runThread, currentThread);
- XCTAssertTrue([context shouldStop]);
}
- (void)testPerformBlockInBackground {
- GTMUnitTestingBooleanRunLoopContext *context =
- [GTMUnitTestingBooleanRunLoopContext context];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"BlockRan"];
__block NSThread *runThread = nil;
[NSThread gtm_performBlockInBackground:^{
runThread = [NSThread currentThread];
- [context setShouldStop:YES];
+ [expectation fulfill];
}];
- XCTAssertTrue([[NSRunLoop currentRunLoop]
- gtm_runUpToSixtySecondsWithContext:context]);
+ [self waitForExpectationsWithTimeout:kTestTimeout handler:NULL];
XCTAssertNotNil(runThread);
XCTAssertNotEqualObjects(runThread, [NSThread currentThread]);
}
@@ -100,151 +86,103 @@
XCTAssertFalse([worker isExecuting]);
XCTAssertFalse([worker isFinished]);
- // Unstarted worker can be stopped without error.
- [worker stop];
- XCTAssertFalse([worker isExecuting]);
- XCTAssertTrue([worker isFinished]);
- // And can be stopped again
- [worker stop];
+ // Unstarted worker can be cancelled without error.
+ [worker cancel];
XCTAssertFalse([worker isExecuting]);
- XCTAssertTrue([worker isFinished]);
+ XCTAssertFalse([worker isFinished]);
- // A thread we start can be stopped with correct state.
- worker = [[GTMSimpleWorkerThread alloc] init];
+ // And can be cancelled again
+ [worker cancel];
XCTAssertFalse([worker isExecuting]);
XCTAssertFalse([worker isFinished]);
- [worker start];
- XCTAssertTrue([worker isExecuting]);
- XCTAssertFalse([worker isFinished]);
- [worker stop];
- XCTAssertFalse([worker isExecuting]);
- XCTAssertTrue([worker isFinished]);
+ [worker release];
- // A cancel is also honored
+ // A thread we start can be cancelled with correct state.
worker = [[GTMSimpleWorkerThread alloc] init];
XCTAssertFalse([worker isExecuting]);
XCTAssertFalse([worker isFinished]);
+ XCTestExpectation *blockPerformed = [self expectationWithDescription:@"BlockIsRunning"];
[worker start];
+ [workerThread_ gtm_performWaitingUntilDone:YES block:^{
+ [blockPerformed fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:kTestTimeout handler:NULL];
XCTAssertTrue([worker isExecuting]);
+ XCTAssertFalse([worker isCancelled]);
XCTAssertFalse([worker isFinished]);
+ NSPredicate *predicate =
+ [NSPredicate predicateWithBlock:^BOOL(id workerThread, NSDictionary<NSString *,id> *options) {
+ return (BOOL)(![workerThread isExecuting]);
+ }];
+ [self expectationForPredicate:predicate evaluatedWithObject:worker handler:NULL];
+
[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);
+ [self waitForExpectationsWithTimeout:kTestTimeout handler:NULL];
XCTAssertFalse([worker isExecuting]);
+ XCTAssertTrue([worker isCancelled]);
XCTAssertTrue([worker isFinished]);
-}
-
-- (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:^{
- [threadLock lock];
- [threadLock unlockWithCondition:1];
- [NSThread sleepForTimeInterval:.25];
- }];
- [threadLock lockWhenCondition:1];
- [threadLock unlock];
- [workerThread_ stop];
- XCTAssertFalse([workerThread_ isExecuting]);
- XCTAssertTrue([workerThread_ isFinished]);
- XCTAssertEqualWithAccuracy(-[start timeIntervalSinceNow], 0.25, 0.25);
+ [worker release];
}
- (void)testPerformBlockOnWorkerThread {
- GTMUnitTestingBooleanRunLoopContext *context =
- [GTMUnitTestingBooleanRunLoopContext context];
__block NSThread *runThread = nil;
// Runs on the other thread
- runThread = nil;
- [context setShouldStop:NO];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"BlockRan"];
[workerThread_ gtm_performBlock:^{
runThread = [NSThread currentThread];
- [context setShouldStop:YES];
+ [expectation fulfill];
}];
- XCTAssertTrue([[NSRunLoop currentRunLoop]
- gtm_runUpToSixtySecondsWithContext:context]);
+ [self waitForExpectationsWithTimeout:kTestTimeout handler:NULL];
XCTAssertNotNil(runThread);
XCTAssertEqualObjects(runThread, workerThread_);
// Other thread no wait.
runThread = nil;
- [context setShouldStop:NO];
+ expectation = [self expectationWithDescription:@"BlockRan2"];
[workerThread_ gtm_performWaitingUntilDone:NO block:^{
runThread = [NSThread currentThread];
- [context setShouldStop:YES];
+ [expectation fulfill];
}];
- XCTAssertTrue([[NSRunLoop currentRunLoop]
- gtm_runUpToSixtySecondsWithContext:context]);
+ [self waitForExpectationsWithTimeout:kTestTimeout handler:NULL];
XCTAssertNotNil(runThread);
XCTAssertEqualObjects(runThread, workerThread_);
// Waiting requires no runloop spin
runThread = nil;
- [context setShouldStop:NO];
[workerThread_ gtm_performWaitingUntilDone:YES block:^{
runThread = [NSThread currentThread];
- [context setShouldStop:YES];
}];
- XCTAssertTrue([context shouldStop]);
XCTAssertNotNil(runThread);
XCTAssertEqualObjects(runThread, workerThread_);
}
-- (void)testExitingBlockIsExecuting {
- NSConditionLock *threadLock = [[[NSConditionLock alloc] initWithCondition:0]
- autorelease];
+- (void)testExitingBlock {
[workerThread_ gtm_performWaitingUntilDone:NO block:^{
- [threadLock lock];
- [threadLock unlockWithCondition:1];
- pthread_exit(NULL);
+ pthread_exit(NULL);
}];
- [threadLock lockWhenCondition:1];
- [threadLock unlock];
- // Give the pthread_exit() a bit of time
- [NSThread sleepForTimeInterval:.25];
- // Did we notice the thread died? Does [... isExecuting] clean up?
- XCTAssertFalse([workerThread_ isExecuting]);
- XCTAssertTrue([workerThread_ isFinished]);
-}
-
-- (void)testExitingBlockCancel {
- NSConditionLock *threadLock = [[[NSConditionLock alloc] initWithCondition:0]
- autorelease];
- [workerThread_ gtm_performWaitingUntilDone:NO block:^{
- [threadLock lock];
- [threadLock unlockWithCondition:1];
- pthread_exit(NULL);
+ NSPredicate *predicate =
+ [NSPredicate predicateWithBlock:^BOOL(id workerThread, NSDictionary<NSString *,id> *options) {
+ return (BOOL)(![workerThread isExecuting]);
}];
- [threadLock lockWhenCondition:1];
- [threadLock unlock];
- // Give the pthread_exit() a bit of time
- [NSThread sleepForTimeInterval:.25];
- // Cancel/stop the thread
- [workerThread_ stop];
- // Did we notice the thread died? Did we clean up?
- XCTAssertFalse([workerThread_ isExecuting]);
+ [self expectationForPredicate:predicate evaluatedWithObject:workerThread_ handler:NULL];
+ [self waitForExpectationsWithTimeout:kTestTimeout handler:NULL];
XCTAssertTrue([workerThread_ isFinished]);
}
-- (void)testStopFromThread {
- NSConditionLock *threadLock = [[[NSConditionLock alloc] initWithCondition:0]
- autorelease];
+
+
+- (void)testCancelFromThread {
[workerThread_ gtm_performWaitingUntilDone:NO block:^{
- [threadLock lock];
- [workerThread_ stop]; // Shold not block.
- [threadLock unlockWithCondition:1];
+ [workerThread_ cancel];
+ }];
+ NSPredicate *predicate =
+ [NSPredicate predicateWithBlock:^BOOL(id workerThread, NSDictionary<NSString *,id> *options) {
+ return (BOOL)(![workerThread isExecuting]);
}];
- // 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(1);
- XCTAssertFalse([workerThread_ isExecuting]);
+ [self expectationForPredicate:predicate evaluatedWithObject:workerThread_ handler:NULL];
+ [self waitForExpectationsWithTimeout:kTestTimeout handler:NULL];
XCTAssertTrue([workerThread_ isFinished]);
}