Skip to content

Instantly share code, notes, and snippets.

@iwasrobbed
Created September 5, 2019 22:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iwasrobbed/223f5790f99a6b2d7ba51cf3c0573b18 to your computer and use it in GitHub Desktop.
Save iwasrobbed/223f5790f99a6b2d7ba51cf3c0573b18 to your computer and use it in GitHub Desktop.
//
// 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)
}
}
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