360 lines
14 KiB
Mathematica
360 lines
14 KiB
Mathematica
|
|
/*
|
||
|
|
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
|