-
-
Save calebd/93fa347397cec5f88233 to your computer and use it in GitHub Desktop.
import Foundation | |
/// An abstract class that makes building simple asynchronous operations easy. | |
/// Subclasses must implement `execute()` to perform any work and call | |
/// `finish()` when they are done. All `NSOperation` work will be handled | |
/// automatically. | |
open class AsynchronousOperation: Operation { | |
// MARK: - Properties | |
private let stateQueue = DispatchQueue( | |
label: "com.calebd.operation.state", | |
attributes: .concurrent) | |
private var rawState = OperationState.ready | |
@objc private dynamic var state: OperationState { | |
get { | |
return stateQueue.sync(execute: { rawState }) | |
} | |
set { | |
willChangeValue(forKey: "state") | |
stateQueue.sync( | |
flags: .barrier, | |
execute: { rawState = newValue }) | |
didChangeValue(forKey: "state") | |
} | |
} | |
public final override var isReady: Bool { | |
return state == .ready && super.isReady | |
} | |
public final override var isExecuting: Bool { | |
return state == .executing | |
} | |
public final override var isFinished: Bool { | |
return state == .finished | |
} | |
public final override var isAsynchronous: Bool { | |
return true | |
} | |
// MARK: - NSObject | |
@objc private dynamic class func keyPathsForValuesAffectingIsReady() -> Set<String> { | |
return ["state"] | |
} | |
@objc private dynamic class func keyPathsForValuesAffectingIsExecuting() -> Set<String> { | |
return ["state"] | |
} | |
@objc private dynamic class func keyPathsForValuesAffectingIsFinished() -> Set<String> { | |
return ["state"] | |
} | |
// MARK: - Foundation.Operation | |
public override final func start() { | |
super.start() | |
if isCancelled { | |
finish() | |
return | |
} | |
state = .executing | |
execute() | |
} | |
// MARK: - Public | |
/// Subclasses must implement this to perform their work and they must not | |
/// call `super`. The default implementation of this function throws an | |
/// exception. | |
open func execute() { | |
fatalError("Subclasses must implement `execute`.") | |
} | |
/// Call this function after any work is done or after a call to `cancel()` | |
/// to move the operation into a completed state. | |
public final func finish() { | |
state = .finished | |
} | |
} | |
@objc private enum OperationState: Int { | |
case ready | |
case executing | |
case finished | |
} |
@calebd So do you set state = .Executing
at the start of all of your overridden start
methods, as well as check isCancelled
, etc.? I recently implemented one of these in Objective-C, following Apple’s guidelines, and ended up with something that looked like:
- (void)start {
if (self.isCancelled) {
self.finished = YES;
}
else {
[self main];
self.executing = YES;
}
}
@irace Yep! I typically end up with something just like that.
Excellent work. Very helpful. Thanks for sharing!
Thanks for sharing! I'm wondering for which use cases are you using asynchronous operations, like the one here, in your applications?.
It seems there is no point at all in using this kind of async implementation of operations if we are running our operations using NSOperationQueues. So I'm trying to think an example in which it would be suitable to use a standalone async op.
I'm basing my comment on this section of Apple Docs:
"When you add an operation to an operation queue, the queue ignores the value of the asynchronous property and always calls the start method from a separate thread. Therefore, if you always run operations by adding them to an operation queue, there is no reason to make them asynchronous."
Asynchronous Versus Synchronous Operations section of:
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/NSOperation_class/#//apple_ref/c/tdef/NSOperationQueuePriority
@barbaramartina sometimes you work with libraries that themselves are asynchronous. You either have to deal with delegates or blocks and can't run all the code from within main
.
For those interested in the blog post, it moved here: http://calebmdavenport.tumblr.com/post/123520933906/swift-concurrent-operations
Don't the getters and setters for state need to be thread safe? I'm thinking there needs to be some sort of locking mechanism
This works well. Help me save a lot of time.
Thank you.
I am learning to use the Operation
class. Can I ask what the following is used for?
// MARK: - NSObject
@objc private dynamic class func keyPathsForValuesAffectingIsReady() -> Set<String> {
return ["state"]
}
@objc private dynamic class func keyPathsForValuesAffectingIsExecuting() -> Set<String> {
return ["state"]
}
@objc private dynamic class func keyPathsForValuesAffectingIsFinished() -> Set<String> {
return ["state"]
}
@nickkohrn That tells KVO that any change to state
is implicitly a change to isReady
, isExecuting
, and isFinished
.
Thanks for the nice abstraction! I see that you are using a concurrent dispatch queue and the barrier API for the read and write access of the state variable. Is it equivalent of using a serial dispatch queue? Will there be some difference? Once again, great work, thank you!
Do not call super.start()
from your override
of start
. As the documentation for start
says (emphasis added):
If you are implementing a concurrent operation, you must override this method and use it to initiate your operation. Your custom implementation must not call
super
at any time.
@diwu This pattern is known as "reader-writer" synchronization, allowing for concurrent reads, but all writes are synchronized. This is like the serial dispatch queue pattern, but is conceptually a little more efficient. The difference is only material in high-contention environments, but it doesn't hurt.
BTW, the reader-writer pattern is discussed in the latter part of WWDC 2012 video Asynchronous Design Patterns with Blocks, GCD, and XPC. Note, that video is using the old Swift 2 and Objective-C GCD syntax, but the idea is identical to what you see here.
By the way, I notice that this sample is doing the manual KVN of state
. Because it’s a dynamic
property, that is not needed. It does the KVN for you.
@calebd What's the licence of the this gist?
Thank you for sharing this.
I am learning to use the
Operation
class. Can I ask what the following is used for?// MARK: - NSObject @objc private dynamic class func keyPathsForValuesAffectingIsReady() -> Set<String> { return ["state"] } @objc private dynamic class func keyPathsForValuesAffectingIsExecuting() -> Set<String> { return ["state"] } @objc private dynamic class func keyPathsForValuesAffectingIsFinished() -> Set<String> { return ["state"] }
Here is Registering Dependent Keys
This is really great, except that because the
Ready
state is set on init, dependencies are ignored. In the superclass's implementation,ready
does not return true unless all dependencies are completed.I changed the getter for
ready
toreturn super.ready && state == .Ready
in case I want to delay the ready state ininit
, while also respecting dependencies.(Also - the first line of
init
could just bestate = .Ready
like you have used everywhere else)