How can an NSOperationQueue wait for two async operations?

后端 未结 3 1423
情书的邮戳
情书的邮戳 2020-12-30 10:15

How can I make an NSOperationQueue (or anything else) wait for two async network calls with callbacks? The flow needs to look like this

Block Begins {
    Ne         


        
3条回答
  •  爱一瞬间的悲伤
    2020-12-30 10:31

    Using Grand Central Dispatch and DispatchGroup

    With Swift 3, in the simplest cases where you don't need fine grained control on tasks states, you can use Grand Central Dispatch and DispatchGroup. The following Playground code shows how it works:

    import Foundation
    import PlaygroundSupport
    
    PlaygroundPage.current.needsIndefiniteExecution = true
    
    let group = DispatchGroup()
    
    group.enter()
    // Perform some asynchronous operation
    let queue1 = DispatchQueue(label: "com.example.imagetransform")
    queue1.async {
        print("Task One finished")
        group.leave()
    }
    
    group.enter()
    // Perform some asynchronous operation
    let queue2 = DispatchQueue(label: "com.example.retrievedata")
    queue2.async {
        print("Task Two finished")
        group.leave()
    }
    
    group.notify(queue: DispatchQueue.main, execute: { print("Task Three finished") })
    

    The previous code will print "Task Three finished" once the two asynchronous tasks are both finished.


    Using OperationQueue and Operation

    Using OperationQueue and Operation for your request tasks requires more boilerplate code but offers many advantages like kvoed state and dependency.

    1. Create an Operation subclass that will act as an abstract class

    import Foundation
    
    /**
     NSOperation documentation:
     Operation objects are synchronous by default.
     At no time in your start method should you ever call super.
     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.
     If you are creating a concurrent operation, you need to override the following methods and properties at a minimum:
     start, asynchronous, executing, finished.
     */
    
    open class AbstractOperation: Operation {
        
        @objc enum State: Int {
            case isReady, isExecuting, isFinished
            
            func canTransition(toState state: State) -> Bool {
                switch (self, state) {
                case (.isReady, .isExecuting):      return true
                case (.isReady, .isFinished):       return true
                case (.isExecuting, .isFinished):   return true
                default:                            return false
                }
            }
        }
    
        // use the KVO mechanism to indicate that changes to `state` affect other properties as well
        class func keyPathsForValuesAffectingIsReady() -> Set {
            return [#keyPath(state) as NSObject]
        }
        
        class func keyPathsForValuesAffectingIsExecuting() -> Set {
            return [#keyPath(state) as NSObject]
        }
        
        class func keyPathsForValuesAffectingIsFinished() -> Set {
            return [#keyPath(state) as NSObject]
        }
        
        // A lock to guard reads and writes to the `_state` property
        private let stateLock = NSLock()
        
        private var _state = State.isReady
        var state: State {
            get {
                stateLock.lock()
                let value = _state
                stateLock.unlock()
                return value
            }
            set (newState) {
                // Note that the KVO notifications MUST NOT be called from inside the lock. If they were, the app would deadlock.
                willChangeValue(forKey: #keyPath(state))
                
                stateLock.lock()
                if _state == .isFinished {
                    assert(_state.canTransition(toState: newState), "Performing invalid state transition from \(_state) to \(newState).")
                    _state = newState
                }
                stateLock.unlock()
                
                didChangeValue(forKey: #keyPath(state))
            }
        }
        
        override open var isExecuting: Bool {
            return state == .isExecuting
        }
        
        override open var isFinished: Bool {
            return state == .isFinished
        }
        
        var hasCancelledDependencies: Bool {
            // Return true if this operation has any dependency (parent) operation that is cancelled
            return dependencies.reduce(false) { $0 || $1.isCancelled }
        }
        
        override final public func start() {
            // If any dependency (parent operation) is cancelled, we should also cancel this operation
            if hasCancelledDependencies {
                finish()
                return
            }
            
            if isCancelled {
                finish()
                return
            }
            
            state = .isExecuting
            main()
        }
        
        open override func main() {
            fatalError("This method has to be overriden and has to call `finish()` at some point")
        }
        
        open func didCancel() {
            finish()
        }
        
        open func finish() {
            state = .isFinished
        }
        
    }
    

    2. Create your operations

    import Foundation
    
    open class CustomOperation1: AbstractOperation {
        
        override open func main() {
            if isCancelled {
                finish()
                return
            }
            
            // Perform some asynchronous operation
            let queue = DispatchQueue(label: "com.app.serialqueue1")
            let delay = DispatchTime.now() + .seconds(5)
            queue.asyncAfter(deadline: delay) {
                self.finish()
                print("\(self) finished")
            }
        }
        
    }
    
    import Foundation
    
    open class CustomOperation2: AbstractOperation {
        
        override open func main() {
            if isCancelled {
                finish()
                return
            }
            
            // Perform some asynchronous operation
            let queue = DispatchQueue(label: "com.app.serialqueue2")
            queue.async {
                self.finish()
                print("\(self) finished")
            }
        }
        
    }
    

    3. Usage

    import Foundation
    import PlaygroundSupport
    
    PlaygroundPage.current.needsIndefiniteExecution = true
    
    // Declare operations
    let operation1 = CustomOperation1()
    let operation2 = CustomOperation2()
    let operation3 = CustomOperation1()
    
    // Set operation3 to perform only after operation1 and operation2 have finished
    operation3.addDependency(operation2)
    operation3.addDependency(operation1)
    
    // Launch operations
    let queue = OperationQueue()
    queue.addOperations([operation2, operation3, operation1], waitUntilFinished: false)
    

    With this code, operation3 is guaranteed to always be performed last.


    You can find this Playground on this GitHub repo.

提交回复
热议问题