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
}
@bygri
Copy link

bygri commented Sep 12, 2014

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 to return super.ready && state == .Ready in case I want to delay the ready state in init, while also respecting dependencies.

(Also - the first line of init could just be state = .Ready like you have used everywhere else)

@irace
Copy link

irace commented Mar 3, 2015

@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;
    }
}

@calebd
Copy link
Author

calebd commented Mar 3, 2015

@irace Yep! I typically end up with something just like that.

@jamesbebbington
Copy link

Hey @calebd have you taken your blog offline? The URL referenced in this gist is no longer resolving.

Could you post what it said here maybe?

Thanks.

@jeremyconkin
Copy link

Excellent work. Very helpful. Thanks for sharing!

@barbaramartina
Copy link

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

@aaronrogers
Copy link

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

@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