Networking/QRunLoopOperation.m
/* |
File: QRunLoopOperation.m |
Contains: An abstract subclass of NSOperation for async run loop based operations. |
Written by: DTS |
Copyright: Copyright (c) 2010 Apple Inc. All Rights Reserved. |
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. |
("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or |
redistribution of this Apple software constitutes acceptance of |
these terms. If you do not agree with these terms, please do |
not use, install, modify or redistribute this Apple software. |
In consideration of your agreement to abide by the following |
terms, and subject to these terms, Apple grants you a personal, |
non-exclusive license, under Apple's copyrights in this |
original Apple software (the "Apple Software"), to use, |
reproduce, modify and redistribute the Apple Software, with or |
without modifications, in source and/or binary forms; provided |
that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the |
following text and disclaimers in all such redistributions of |
the Apple Software. Neither the name, trademarks, service marks |
or logos of Apple Inc. may be used to endorse or promote |
products derived from the Apple Software without specific prior |
written permission from Apple. Except as expressly stated in |
this notice, no other rights or licenses, express or implied, |
are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or |
by other works in which the Apple Software may be incorporated. |
The Apple Software is provided by Apple on an "AS IS" basis. |
APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING |
WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, |
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING |
THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN |
COMBINATION WITH YOUR PRODUCTS. |
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, |
INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED |
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY |
OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION |
OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY |
OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR |
OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF |
SUCH DAMAGE. |
*/ |
#import "QRunLoopOperation.h" |
/* |
Theory of Operation |
------------------- |
Some critical points: |
1. By the time we're running on the run loop thread, we know that all further state |
transitions happen on the run loop thread. That's because there are only three |
states (inited, executing, and finished) and run loop thread code can only run |
in the last two states and the transition from executing to finished is |
always done on the run loop thread. |
2. -start can only be called once. So run loop thread code doesn't have to worry |
about racing with -start because, by the time the run loop thread code runs, |
-start has already been called. |
3. -cancel can be called multiple times from any thread. Run loop thread code |
must take a lot of care with do the right thing with cancellation. |
Some state transitions: |
1. init -> dealloc |
2. init -> cancel -> dealloc |
XXX 3. init -> cancel -> start -> finish -> dealloc |
4. init -> cancel -> start -> startOnRunLoopThreadThread -> finish dealloc |
!!! 5. init -> start -> cancel -> startOnRunLoopThreadThread -> finish -> cancelOnRunLoopThreadThread -> dealloc |
XXX 6. init -> start -> cancel -> cancelOnRunLoopThreadThread -> startOnRunLoopThreadThread -> finish -> dealloc |
XXX 7. init -> start -> cancel -> startOnRunLoopThreadThread -> cancelOnRunLoopThreadThread -> finish -> dealloc |
8. init -> start -> startOnRunLoopThreadThread -> finish -> dealloc |
9. init -> start -> startOnRunLoopThreadThread -> cancel -> cancelOnRunLoopThreadThread -> finish -> dealloc |
!!! 10. init -> start -> startOnRunLoopThreadThread -> cancel -> finish -> cancelOnRunLoopThreadThread -> dealloc |
11. init -> start -> startOnRunLoopThreadThread -> finish -> cancel -> dealloc |
Markup: |
XXX means that the case doesn't happen. |
!!! means that the case is interesting. |
Described: |
1. It's valid to allocate an operation and never run it. |
2. It's also valid to allocate an operation, cancel it, and yet never run it. |
3. While it's valid to cancel an operation before it starting it, this case doesn't |
happen because -start always bounces to the run loop thread to maintain the invariant |
that the executing to finished transition always happens on the run loop thread. |
4. In this -startOnRunLoopThread detects the cancellation and finishes immediately. |
5. Because the -cancel can happen on any thread, it's possible for the -cancel |
to come in between the -start and the -startOnRunLoop thread. In this case |
-startOnRunLoopThread notices isCancelled and finishes straightaway. And |
-cancelOnRunLoopThread detects that the operation is finished and does nothing. |
6. This case can never happen because -performSelecton:onThread:xxx |
callbacks happen in order, -start is synchronised with -cancel, and -cancel |
only schedules if -start has run. |
7. This case can never happen because -startOnRunLoopThread will finish immediately |
if it detects isCancelled (see case 5). |
8. This is the standard run-to-completion case. |
9. This is the standard cancellation case. -cancelOnRunLoopThread wins the race |
with finish, and it detects that the operation is executing and actually cancels. |
10. In this case the -cancelOnRunLoopThread loses the race with finish, but that's OK |
because -cancelOnRunLoopThread already does nothing if the operation is already |
finished. |
11. Cancellating after finishing still sets isCancelled but has no impact |
on the RunLoop thread code. |
*/ |
@interface QRunLoopOperation () |
// read/write versions of public properties |
@property (assign, readwrite) QRunLoopOperationState state; |
@property (copy, readwrite) NSError * error; |
@end |
@implementation QRunLoopOperation |
- (id)init |
{ |
self = [super init]; |
if (self != nil) { |
assert(self->_state == kQRunLoopOperationStateInited); |
} |
return self; |
} |
- (void)dealloc |
{ |
assert(self->_state != kQRunLoopOperationStateExecuting); |
[self->_runLoopModes release]; |
[self->_runLoopThread release]; |
[self->_error release]; |
[super dealloc]; |
} |
#pragma mark * Properties |
@synthesize runLoopThread = _runLoopThread; |
@synthesize runLoopModes = _runLoopModes; |
- (NSThread *)actualRunLoopThread |
// Returns the effective run loop thread, that is, the one set by the user |
// or, if that's not set, the main thread. |
{ |
NSThread * result; |
result = self.runLoopThread; |
if (result == nil) { |
result = [NSThread mainThread]; |
} |
return result; |
} |
- (BOOL)isActualRunLoopThread |
// Returns YES if the current thread is the actual run loop thread. |
{ |
return [[NSThread currentThread] isEqual:self.actualRunLoopThread]; |
} |
- (NSSet *)actualRunLoopModes |
{ |
NSSet * result; |
result = self.runLoopModes; |
if ( (result == nil) || ([result count] == 0) ) { |
result = [NSSet setWithObject:NSDefaultRunLoopMode]; |
} |
return result; |
} |
@synthesize error = _error; |
#pragma mark * Core state transitions |
- (QRunLoopOperationState)state |
{ |
return self->_state; |
} |
- (void)setState:(QRunLoopOperationState)newState |
// Change the state of the operation, sending the appropriate KVO notifications. |
{ |
// any thread |
@synchronized (self) { |
QRunLoopOperationState oldState; |
// The following check is really important. The state can only go forward, and there |
// should be no redundant changes to the state (that is, newState must never be |
// equal to self->_state). |
assert(newState > self->_state); |
// Transitions from executing to finished must be done on the run loop thread. |
assert( (newState != kQRunLoopOperationStateFinished) || self.isActualRunLoopThread ); |
// inited + executing -> isExecuting |
// inited + finished -> isFinished |
// executing + finished -> isExecuting + isFinished |
oldState = self->_state; |
if ( (newState == kQRunLoopOperationStateExecuting) || (oldState == kQRunLoopOperationStateExecuting) ) { |
[self willChangeValueForKey:@"isExecuting"]; |
} |
if (newState == kQRunLoopOperationStateFinished) { |
[self willChangeValueForKey:@"isFinished"]; |
} |
self->_state = newState; |
if (newState == kQRunLoopOperationStateFinished) { |
[self didChangeValueForKey:@"isFinished"]; |
} |
if ( (newState == kQRunLoopOperationStateExecuting) || (oldState == kQRunLoopOperationStateExecuting) ) { |
[self didChangeValueForKey:@"isExecuting"]; |
} |
} |
} |
- (void)startOnRunLoopThread |
// Starts the operation. The actual -start method is very simple, |
// deferring all of the work to be done on the run loop thread by this |
// method. |
{ |
assert(self.isActualRunLoopThread); |
assert(self.state == kQRunLoopOperationStateExecuting); |
if ([self isCancelled]) { |
// We were cancelled before we even got running. Flip the the finished |
// state immediately. |
[self finishWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]]; |
} else { |
[self operationDidStart]; |
} |
} |
- (void)cancelOnRunLoopThread |
// Cancels the operation. |
{ |
assert(self.isActualRunLoopThread); |
// We know that a) state was kQRunLoopOperationStateExecuting when we were |
// scheduled (that's enforced by -cancel), and b) the state can't go |
// backwards (that's enforced by -setState), so we know the state must |
// either be kQRunLoopOperationStateExecuting or kQRunLoopOperationStateFinished. |
// We also know that the transition from executing to finished always |
// happens on the run loop thread. Thus, we don't need to lock here. |
// We can look at state and, if we're executing, trigger a cancellation. |
if (self.state == kQRunLoopOperationStateExecuting) { |
[self finishWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]]; |
} |
} |
- (void)finishWithError:(NSError *)error |
{ |
assert(self.isActualRunLoopThread); |
// error may be nil |
if (self.error == nil) { |
self.error = error; |
} |
[self operationWillFinish]; |
self.state = kQRunLoopOperationStateFinished; |
} |
#pragma mark * Subclass override points |
- (void)operationDidStart |
{ |
assert(self.isActualRunLoopThread); |
} |
- (void)operationWillFinish |
{ |
assert(self.isActualRunLoopThread); |
} |
#pragma mark * Overrides |
- (BOOL)isConcurrent |
{ |
// any thread |
return YES; |
} |
- (BOOL)isExecuting |
{ |
// any thread |
return self.state == kQRunLoopOperationStateExecuting; |
} |
- (BOOL)isFinished |
{ |
// any thread |
return self.state == kQRunLoopOperationStateFinished; |
} |
- (void)start |
{ |
// any thread |
assert(self.state == kQRunLoopOperationStateInited); |
// We have to change the state here, otherwise isExecuting won't necessarily return |
// true by the time we return from -start. Also, we don't test for cancellation |
// here because that would a) result in us sending isFinished notifications on a |
// thread that isn't our run loop thread, and b) confuse the core cancellation code, |
// which expects to run on our run loop thread. Finally, we don't have to worry |
// about races with other threads calling -start. Only one thread is allowed to |
// start us at a time. |
self.state = kQRunLoopOperationStateExecuting; |
[self performSelector:@selector(startOnRunLoopThread) onThread:self.actualRunLoopThread withObject:nil waitUntilDone:NO modes:[self.actualRunLoopModes allObjects]]; |
} |
- (void)cancel |
{ |
BOOL runCancelOnRunLoopThread; |
BOOL oldValue; |
// any thread |
// We need to synchronise here to avoid state changes to isCancelled and state |
// while we're running. |
@synchronized (self) { |
oldValue = [self isCancelled]; |
// Call our super class so that isCancelled starts returning true immediately. |
[super cancel]; |
// If we were the one to set isCancelled (that is, we won the race with regards |
// other threads calling -cancel) and we're actually running (that is, we lost |
// the race with other threads calling -start and the run loop thread finishing), |
// we schedule to run on the run loop thread. |
runCancelOnRunLoopThread = ! oldValue && self.state == kQRunLoopOperationStateExecuting; |
} |
if (runCancelOnRunLoopThread) { |
[self performSelector:@selector(cancelOnRunLoopThread) onThread:self.actualRunLoopThread withObject:nil waitUntilDone:YES modes:[self.actualRunLoopModes allObjects]]; |
} |
} |
@end |
Copyright © 2010 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2010-10-22