Skip to content

Instantly share code, notes, and snippets.

@AliSoftware
Last active December 25, 2015 06:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AliSoftware/6935128 to your computer and use it in GitHub Desktop.
Save AliSoftware/6935128 to your computer and use it in GitHub Desktop.
Demo of a case where resetting onceToken can be non-thread-safe and dangerous
dispatch_once_t onceToken;
SomeClass* _sharedInstance;
+ (id)sharedInstance
{
dispatch_once(&onceToken, ^{ _sharedInstance = [SomeClass new]; });
return _sharedInstance;
}
- (void)tearDown
{
// reset the once token and sharedInstance
onceToken = 0;
_sharedInstance = nil;
[super tearDown];
}
- (void)testFoo
{
__block BOOL _done = NO;
[self doSomeLongTaskWithCompletion:^{
// We should check if the test hasn't timed out yet somewhere here, but suppose I'm a newbie and I don't think about that
// if (_done) return;
[[self sharedInstance] bar]; // do something with sharedInstance, which will access onceToken somehow.
_done = YES;
}];
// Wait for at max 3s (timeout) for the async action to finish
NSDate* timeoutDate = [NSDate dateWithTimeIntervalSinceNow:3];
while (!_done && ([timeoutDate timeIntervalSinceNow]>0))
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.01, YES);
STAssertTrue(_done, @"Test timed out");
// _done = YES; // to mark that the test timed out for the completion block — suppose as a newbie we didn't think of that
}
- (void)doSomeLongTaskWithCompletion:(dispatch_block_t)completion
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(3); // imagine this is a long task, taking more or less than 3 seconds
completion();
});
}
/* The problem:
- The test will time out after 3 seconds, maybe before the "doSomeLongTaskWithCompletion:" method completes.
- The test will then fail, and the -tearDown method will be called… cleaning the onceToken.
But the code dispatched in the global queue is still running in the background even if the test has ended!
- And at quite the same time, the completion block will be called and will call sharedInstance
At that point, you have a risk to set onceToken in an thread-unsafe manner in your tearDown while accessing it for reading in the completionBlock thru the call to sharedInstance.
Or worse, if the completion block is called after the tearDown method has began, the onceToken can even have been resetted,
then the completion block interrupts the main thread and calls the +sharedInstance method,
and at last we go back in the main thread and set _sharedInstance to nil… quite a mess
The problem can be solved if you make sure the code in the completion block is aborted if the test has already finished, but still not anyone think about these subtleties.
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment