Skip to content

Instantly share code, notes, and snippets.

@calebd
Last active April 29, 2023 13:12
Show Gist options
  • Save calebd/93fa347397cec5f88233 to your computer and use it in GitHub Desktop.
Save calebd/93fa347397cec5f88233 to your computer and use it in GitHub Desktop.
Concurrent NSOperation in Swift
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
}
@KingOfBrian
Copy link

For those interested in the blog post, it moved here: http://calebmdavenport.tumblr.com/post/123520933906/swift-concurrent-operations

@freaknbigpanda
Copy link

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

@trthtai
Copy link

trthtai commented Apr 21, 2017

This works well. Help me save a lot of time.
Thank you.

@nickkohrn
Copy link

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"]
}

@calebd
Copy link
Author

calebd commented May 6, 2017

@nickkohrn That tells KVO that any change to state is implicitly a change to isReady, isExecuting, and isFinished.

@diwu
Copy link

diwu commented Aug 24, 2017

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!

@robertmryan
Copy link

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.

@robertmryan
Copy link

robertmryan commented Jan 4, 2018

@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.

@robertmryan
Copy link

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.

@jleach
Copy link

jleach commented Apr 12, 2018

@calebd What's the licence of the this gist?

@lukas2
Copy link

lukas2 commented Mar 11, 2019

Thank you for sharing this.

@YuanfuC
Copy link

YuanfuC commented May 17, 2021

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment