-
-
Save iwasrobbed/223f5790f99a6b2d7ba51cf3c0573b18 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ConcurrentOperation.swift | |
// lib | |
// | |
// | |
// | |
// | |
import Foundation | |
/// An abstract class for building concurrent operations. | |
/// Subclasses must implement `execute()` to perform any work and call | |
/// `finish()` when they are done. All `Operation` work will be handled | |
/// automatically. | |
/// | |
/// Source modified from https://gist.github.com/calebd/93fa347397cec5f88233 and https://gist.github.com/ole/5034ce19c62d248018581b1db0eabb2b | |
/// | |
open class ConcurrentOperation: Foundation.Operation { | |
// MARK: - API | |
// Note: per docs, do not call `super` for concurrent operations | |
open override func start() { | |
if self.isCancelled { | |
self.finish() | |
return | |
} | |
self.state = .running | |
self.main() | |
} | |
open override func cancel() { | |
super.cancel() | |
if self.state == .running { | |
self.finish() | |
} | |
} | |
/// 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 after any work is done or after a call to `cancel()` | |
/// to move the operation into a completed state | |
public final func finish() { | |
self.state = .finished | |
} | |
// MARK: - Public Properties | |
/// Note: this uses two separate queues (`stateQueue` and the underlying queue in `AtomicValue`) to prevent deadlocks when `willChangeValue` and `didChangeValue` are called since they trigger reads from other properties. | |
@objc public var state: OperationState { | |
get { return self.rawState.value } | |
set { | |
// A state mutation should be a single atomic transaction. We can't simply perform | |
// everything on the isolation queue for `rawState` 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. | |
self.stateQueue.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 = self.rawState.value | |
guard newValue != oldValue else { | |
return | |
} | |
willChangeValue(forKey: oldValue.objcKeyPath) | |
willChangeValue(forKey: newValue.objcKeyPath) | |
self.rawState.mutate { | |
$0 = newValue | |
} | |
didChangeValue(forKey: oldValue.objcKeyPath) | |
didChangeValue(forKey: newValue.objcKeyPath) | |
} | |
} | |
} | |
@objc public enum OperationState: Int, CustomStringConvertible { | |
case ready, running, finished | |
/// The `#keyPath` for the `Operation` property that's associated with this value. | |
var objcKeyPath: String { | |
switch self { | |
case .ready: return #keyPath(isReady) | |
case .running: return #keyPath(isExecuting) | |
case .finished: return #keyPath(isFinished) | |
} | |
} | |
public var description: String { | |
switch self { | |
case .ready: return "ready" | |
case .running: return "running" | |
case .finished: return "finished" | |
} | |
} | |
} | |
// MARK: - Overriden Properties | |
public final override var isReady: Bool { return self.state == .ready && super.isReady } | |
public final override var isExecuting: Bool { return self.state == .running } | |
public final override var isFinished: Bool { return self.state == .finished } | |
public final override var isConcurrent: Bool { return true } | |
open override var description: String { | |
return self.debugDescription | |
} | |
open override var debugDescription: String { | |
return "\(type(of: self)) — \(self.name ?? "nil") – \(self.isCancelled ? "cancelled" : String(describing: self.state))" | |
} | |
// MARK: - KVO | |
@objc private static let keyPathsForValuesAffectingIsExecuting: Set<String> = [#keyPath(state)] | |
@objc private static let keyPathsForValuesAffectingIsFinished: Set<String> = [#keyPath(state)] | |
@objc private static let keyPathsForValuesAffectingIsReady: Set<String> = [#keyPath(state)] | |
// MARK: - Private Properties | |
private let stateQueue = DispatchQueue(label: "com.myapp.lib.operation.state") | |
/// Private backing store for `state` | |
private var rawState: AtomicValue<OperationState> = AtomicValue(.ready) | |
} | |
// MARK: - Atomic | |
/// A wrapper for atomic read/write access to a value. | |
/// The value is protected by a serial `DispatchQueue`. | |
/// Source: https://gist.github.com/ole/5034ce19c62d248018581b1db0eabb2b | |
public final class AtomicValue<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) { | |
self._value = value | |
self.queue = DispatchQueue(label: "com.myapp.value.AtomicValue", target: targetQueue) | |
} | |
/// Read access to the wrapped value. | |
public var value: A { | |
return self.queue.sync { self._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) { | |
self.queue.sync { | |
transform(&self._value) | |
} | |
} | |
} | |
extension AtomicValue: Equatable where A: Equatable { | |
public static func ==(lhs: AtomicValue, rhs: AtomicValue) -> Bool { | |
return lhs.value == rhs.value | |
} | |
} | |
extension AtomicValue: Hashable where A: Hashable { | |
public func hash(into hasher: inout Hasher) { | |
hasher.combine(self.value) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
0 Foundation ___NSOQSchedule | |
1 Foundation ___NSOQSchedule | |
2 Foundation +[__NSOperationInternal _observeValueForKeyPath:ofObject:changeKind:oldValue:newValue:indexes:context:] | |
3 Foundation _NSKeyValueNotifyObserver | |
4 Foundation _NSKeyValueDidChange | |
5 Foundation _NSKeyValueDidChangeWithPerThreadPendingNotifications.llvm.1058108052746541926 | |
6 MyApp ConcurrentOperation.swift:77:17 closure #1 () -> () in lib.ConcurrentOperation.state.setter : lib.ConcurrentOperation.OperationState | |
7 MyApp <compiler-generated> partial apply forwarder for reabstraction thunk helper from @callee_guaranteed () -> () to @escaping @callee_guaranteed () -> () | |
8 MyApp <compiler-generated> reabstraction thunk helper from @escaping @callee_guaranteed () -> () to @callee_unowned @convention(block) () -> () | |
9 libdispatch.dylib __dispatch_client_callout | |
10 libdispatch.dylib __dispatch_lane_barrier_sync_invoke_and_complete | |
11 MyApp ConcurrentOperation.swift:64:29 state.set | |
12 MyApp MyApp merged @objc lib.ConcurrentOperation.start() -> () | |
13 Foundation ___NSOQSchedule_f | |
14 libdispatch.dylib __dispatch_call_block_and_release | |
15 libdispatch.dylib __dispatch_client_callout | |
16 libdispatch.dylib __dispatch_continuation_pop$VARIANT$mp | |
17 libdispatch.dylib __dispatch_async_redirect_invoke | |
18 libdispatch.dylib __dispatch_root_queue_drain | |
19 libdispatch.dylib __dispatch_worker_thread2 | |
20 libsystem_pthread.dylib __pthread_wqthread |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment