-
Star
(129)
You must be signed in to star a gist -
Fork
(19)
You must be signed in to fork a gist
-
-
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 | |
| } |
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
superat 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
Operationclass. 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
@nickkohrn That tells KVO that any change to
stateis implicitly a change toisReady,isExecuting, andisFinished.