-
-
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 | |
} |
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
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!