Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
An (NS)Operation subclass for async operations
import Foundation
/// An abstract class that makes building simple asynchronous operations easy.
/// Subclasses must override `main()` to perform any work and call `finish()`
/// when they are done. All `NSOperation` work will be handled automatically.
///
/// Source/Inspiration: https://stackoverflow.com/a/48104095/116862 and https://gist.github.com/calebd/93fa347397cec5f88233
open class AsyncOperation: Operation {
public init(name: String? = nil) {
super.init()
self.name = name
}
/// Serial queue for making state changes atomic under the constraint
/// of having to send KVO willChange/didChange notifications.
private let stateChangeQueue = DispatchQueue(label: "com.olebegemann.AsyncOperation.stateChange")
/// Private backing store for `state`
private var _state: Atomic<State> = Atomic(.ready)
/// The state of the operation
private var state: State {
get {
return _state.value
}
set {
// A state mutation should be a single atomic transaction. We can't simply perform
// everything on the isolation queue for `_state` because the KVO willChange/didChange
// notifications have to be sent from outside the isolation queue. Otherwise we would
// deadlock because KVO observers will in turn try to read `state` (by calling
// `isReady`, `isExecuting`, `isFinished`. Use a second queue to wrap the entire
// transaction.
stateChangeQueue.sync {
// Retrieve the existing value first. Necessary for sending fine-grained KVO
// willChange/didChange notifications only for the key paths that actually change.
let oldValue = _state.value
guard newValue != oldValue else {
return
}
willChangeValue(forKey: oldValue.objcKeyPath)
willChangeValue(forKey: newValue.objcKeyPath)
_state.mutate {
$0 = newValue
}
didChangeValue(forKey: oldValue.objcKeyPath)
didChangeValue(forKey: newValue.objcKeyPath)
}
}
}
/// Mirror of the possible states an (NS)Operation can be in
private enum State: Int, CustomStringConvertible {
case ready
case executing
case finished
/// The `#keyPath` for the `Operation` property that's associated with this value.
var objcKeyPath: String {
switch self {
case .ready: return #keyPath(isReady)
case .executing: return #keyPath(isExecuting)
case .finished: return #keyPath(isFinished)
}
}
var description: String {
switch self {
case .ready: return "ready"
case .executing: return "executing"
case .finished: return "finished"
}
}
}
public final override var isAsynchronous: Bool { return true }
open 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
}
// MARK: - Foundation.Operation
public final override func start() {
guard !isCancelled else {
finish()
return
}
state = .executing
main()
}
// MARK: - Public
/// Subclasses must implement this to perform their work and they must not call `super`.
/// The default implementation of this function traps.
open override func main() {
preconditionFailure("Subclasses must implement `main`.")
}
/// Call this function to finish an operation that is currently executing.
/// State can also be "ready" here if the operation was cancelled before it started.
public final func finish() {
if isExecuting || isReady {
state = .finished
}
}
open override var description: String {
return debugDescription
}
open override var debugDescription: String {
return "\(type(of: self))\(name ?? "nil")\(isCancelled ? "cancelled" : String(describing: state))"
}
}
import Dispatch
/// A wrapper for atomic read/write access to a value.
/// The value is protected by a serial `DispatchQueue`.
public final class Atomic<A> {
private var _value: A
private let queue: DispatchQueue
/// Creates an instance of `Atomic` with the specified value.
///
/// - Paramater value: The object's initial value.
/// - Parameter targetQueue: The target dispatch queue for the "lock queue".
/// Use this to place the atomic value into an existing queue hierarchy
/// (e.g. for the subsystem that uses this object).
/// See Apple's WWDC 2017 session 706, Modernizing Grand Central Dispatch
/// Usage (https://developer.apple.com/videos/play/wwdc2017/706/), for
/// more information on how to use target queues effectively.
///
/// The default value is `nil`, which means no target queue will be set.
public init(_ value: A, targetQueue: DispatchQueue? = nil) {
_value = value
queue = DispatchQueue(label: "com.olebegemann.Atomic", target: targetQueue)
}
/// Read access to the wrapped value.
public var value: A {
return queue.sync { _value }
}
/// Mutations of `value` must be performed via this method.
///
/// If `Atomic` exposed a setter for `value`, constructs that used the getter
/// and setter inside the same statement would not be atomic.
///
/// Examples that would not actually be atomic:
///
/// let atomicInt = Atomic(42)
/// // Calls getter and setter, but value may have been mutated in between
/// atomicInt.value += 1
///
/// let atomicArray = Atomic([1,2,3])
/// // Mutating the array through a subscript causes both a get and a set,
/// // acquiring and releasing the lock twice.
/// atomicArray[1] = 42
///
/// See also: https://github.com/ReactiveCocoa/ReactiveSwift/issues/269
public func mutate(_ transform: (inout A) -> Void) {
queue.sync {
transform(&_value)
}
}
}
extension Atomic: Equatable where A: Equatable {
public static func ==(lhs: Atomic, rhs: Atomic) -> Bool {
return lhs.value == rhs.value
}
}
extension Atomic: Hashable where A: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(value)
}
}
@rob-keepsafe

This comment has been minimized.

Copy link

rob-keepsafe commented Sep 5, 2019

Hi @ole 👋 Thanks for sharing this implementation; we've seen with both this implementation and the one it's based off of by Caleb that there are EXC_BAD_ACCESS on didChangeValue(forKey: newValue.objcKeyPath) (https://gist.github.com/ole/5034ce19c62d248018581b1db0eabb2b#file-asyncoperation-swift-L46) due to Attempted to dereference garbage pointer 0x41.

I'm curious if you have an idea why since I'd assume it's protected within the stateQueue

@ole

This comment has been minimized.

Copy link
Owner Author

ole commented Sep 6, 2019

@rob-keepsafe Yes, I have seen similar crashes from time to time, though usually SIGSEGV rather than EXC_BAD_ACCESS. But the backtraces do look similar to yours. I don't have any idea what causes it.

There have been quite a number of multithreading bugs in Swift's KVO overlay up until Swift 5.0, so I have a vague hope that building with Xcode 11/Swift 5.1 will fix things — although we did install the workaround mentioned in the linked PR, so maybe those shouldn't be an issue anyway.

Please let me know if you find something.

@rob-keepsafe

This comment has been minimized.

Copy link

rob-keepsafe commented Sep 6, 2019

Thanks for the quick reply and extra information, @ole 🙇 We're also using the KVO workaround (since it corrupted every operation across the entire sandbox/3rd party libs without it). I might file a radar for this one since it seems pretty overkill to guard it like this and yet KVO still has threading issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.