【问题标题】:How to make inner async request complete first before completing outer async request in Swift?如何在 Swift 中完成外部异步请求之前先完成内部异步请求?
【发布时间】:2016-05-13 05:14:30
【问题描述】:

我一直在努力实现这一目标,但无法让它发挥作用。

首先让我展示一个简单的示例代码:

override func viewDidLoad()
{
    super.viewDidLoad()

    methodOne("some url bring")
}

func methodOne(urlString1: String)
{
    let targetURL = NSURL(string: urlString1)

    let task = NSURLSession.sharedSession().dataTaskWithURL(targetURL!) {(data, response, error) in

        // DO STUFF
        j = some value

        print("Inside Async1")
        for k in j...someArray.count - 1
        {  
            print("k = \(k)")
            print("Calling Async2")
            self.methodTwo("some url string")
        }

    }

    task.resume()
}

func methodTwo(urlString2: String)
{
    let targetURL = NSURL(string: urlString2)

    let task = NSURLSession.sharedSession().dataTaskWithURL(targetURL!) {(data, response, error) in

        // DO STUFF
        print("inside Async2")
    }

    task.resume()
}

我基本上在做的是在我的methodOne 中执行一个异步请求,并在该函数中调用我的methodTwo,它执行另一个异步请求。

我遇到的问题是当methodTwo 被调用时,它永远不会进入异步会话。但是,它确实在methodTwo 内进入异步会话,但只有一次k = someArray.count - 1。基本上是排队到最后,这不是我想要达到的。

这是一个示例输出:

Inside Async1
k = 0
Calling Async2
Inside Async1
k = 0
Calling Async2
k = 1
Calling Async2
Inside Async1
k = 0
Calling Async2
k = 1
Calling Async2
k = 2
Calling Async2
Inside Async1
.....
Inside Async1
k = 0
Calling Async2
k = 1
Calling Async2
k = 2
Calling Async2
k = 3
Calling Async2
k = 4
Inside Async2

换句话说,我希望在来自methodOne 的异步请求完成之前,在每次迭代中完成来自methodTwo 的异步请求。

这是我的目标的示例输出:

Inside Async1
k = 0
Calling Async2
Inside Async2
Inside Async1
k = 1
Calling Async2
Inside Async2
Inside Async1
...

我在这里找到了类似的东西:Wait until first async function is completed then execute the second async function

但是,我无法使用这些建议和解决方案。

有人能指出正确的方向吗?

谢谢

【问题讨论】:

  • 如果 MethodOne 和 MethodTwo 之间有执行顺序依赖关系,为什么要使用两个单独的异步操作?
  • 看看这个非常相似的问题和我最近给出的答案:stackoverflow.com/a/37155037/465677
  • @CouchDeveloper 我不确定我是否了解您的解决方案,甚至不确定如何将其应用于我的问题,因为我是新手

标签: ios swift asynchronous


【解决方案1】:

一种方法是更改​​methodTwo() 以接受回调作为参数,然后您可以使用信号量:

func methodOne(urlString1: String) {
    let targetURL = NSURL(string: urlString1)
    let task = NSURLSession.sharedSession().dataTaskWithURL(targetURL!) { data, response, error in
        let queue = dispatch_queue_create("org.myorg.myqueue", nil)
        dispatch_async(queue) {

            // DO STUFF
            j = some value

            print("Inside Async1")
            for k in j...someArray.count - 1 {  
                print("k = \(k)")

                print("Calling Async2")
                dispatch_semaphore_t sem = dispatch_semaphore_create(0);
                self.methodTwo("some url string") {
                    dispatch_semaphore_signal(sem);
                }
                dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
            }
        }
    }
    task.resume()
}

func methodTwo(urlString2: String, callback: (() -> ())) {
    let targetURL = NSURL(string: urlString2)
    let task = NSURLSession.sharedSession().dataTaskWithURL(targetURL!) { data, response, error in

        // DO STUFF
        print("inside Async2")
        callback()
    }
    task.resume()
}

注意,为了不阻塞methodOne的任务回调的委托队列,示例创建了自己的队列,你可以随意阻塞。

【讨论】:

  • 我不同意阻塞队列就可以了。实际上,您的方法会导致严重的问题:每次执行循环中的代码块时,它都会分派到队列并随后阻塞底层线程。下一次迭代需要一个新线程,因此会产生另一个。由于 GCD 限制了最大线程数(最多 64 个),调用线程(主线程)将阻塞,并可能死锁。
  • @CouchDeveloper 是的,你是对的,显然数据任务总是调用“委托队列”。更新了示例以使用自己的队列。
  • 我将您的建议应用到我的实际代码中,但由于某些代码的执行时间比其他代码早得多,我似乎遇到了一些错误...您能否查看提供的链接中的示例代码?
【解决方案2】:

您应该使用同步请求。 这个扩展很容易使用:

extension NSURLSession {
    public static func requestSynchronousData(request: NSURLRequest, completion: ((data: NSData?, error: NSError?) -> Void)?) {
        var data: NSData? = nil
        var error: NSError? = nil
        let semaphore: dispatch_semaphore_t = dispatch_semaphore_create(0)
        NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: {
            taskData, _, taskError -> () in
            data = taskData
            error = taskError
            if data == nil, let error = error {print(error)}
            dispatch_semaphore_signal(semaphore);
        }).resume()
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
        completion?(data: data, error: error)
    }
}

并在methodTwo发送同步请求:

func methodOne(urlString1: String) {
    guard let targetURL = NSURL(string: urlString1) else { return }
    let request = NSURLRequest(URL: targetURL)
    NSURLSession.sharedSession().dataTaskWithRequest(request) { (data, response, error) in
        // DO STUFF
        print("Inside Async1")
        for k in 0..<5 {
            print("k = \(k)")
            print("Calling Async2")
            self.methodTwo("http://www.google.com")
        }
    }.resume()
}

func methodTwo(urlString2: String) {
    guard let targetURL = NSURL(string: urlString2) else { return }
    let request = NSURLRequest(URL: targetURL)
    NSURLSession.requestSynchronousData(request) { (data, error) in
        // DO STUFF
        print("inside Async2")
    }
}

您也可以使用 Dispatch Queue 对其进行管理。 Learn more about GCD

【讨论】:

    【解决方案3】:

    我会使用自定义的异步NSOperation 子类来处理网络请求,而不是其他人建议的信号量或组(它会阻塞线程,如果阻塞了太多线程可能会出现问题)。将请求封装在异步 NSOperation 中后,您就可以将一堆操作添加到操作队列中,不会阻塞任何线程,而是享受这些异步操作之间的依赖关系。

    例如,网络操作可能如下所示:

    class NetworkOperation: AsynchronousOperation {
    
        private let url: NSURL
        private var requestCompletionHandler: ((NSData?, NSURLResponse?, NSError?) -> ())?
        private var task: NSURLSessionTask?
    
        init(url: NSURL, requestCompletionHandler: (NSData?, NSURLResponse?, NSError?) -> ()) {
            self.url = url
            self.requestCompletionHandler = requestCompletionHandler
    
            super.init()
        }
    
        override func main() {
            task = NSURLSession.sharedSession().dataTaskWithURL(url) { data, response, error in
                self.requestCompletionHandler?(data, response, error)
                self.requestCompletionHandler = nil
                self.completeOperation()
            }
            task?.resume()
        }
    
        override func cancel() {
            requestCompletionHandler = nil
            super.cancel()
            task?.cancel()
        }
    
    }
    
    /// Asynchronous Operation base class
    ///
    /// This class performs all of the necessary KVN of `isFinished` and
    /// `isExecuting` for a concurrent `NSOperation` subclass. So, to developer
    /// a concurrent NSOperation subclass, you instead subclass this class which:
    ///
    /// - must override `main()` with the tasks that initiate the asynchronous task;
    ///
    /// - must call `completeOperation()` function when the asynchronous task is done;
    ///
    /// - optionally, periodically check `self.cancelled` status, performing any clean-up
    ///   necessary and then ensuring that `completeOperation()` is called; or
    ///   override `cancel` method, calling `super.cancel()` and then cleaning-up
    ///   and ensuring `completeOperation()` is called.
    
    public class AsynchronousOperation : NSOperation {
    
        override public var asynchronous: Bool { return true }
    
        private let stateLock = NSLock()
    
        private var _executing: Bool = false
        override private(set) public var executing: Bool {
            get {
                return stateLock.withCriticalScope { _executing }
            }
            set {
                willChangeValueForKey("isExecuting")
                stateLock.withCriticalScope { _executing = newValue }
                didChangeValueForKey("isExecuting")
            }
        }
    
        private var _finished: Bool = false
        override private(set) public var finished: Bool {
            get {
                return stateLock.withCriticalScope { _finished }
            }
            set {
                willChangeValueForKey("isFinished")
                stateLock.withCriticalScope { _finished = newValue }
                didChangeValueForKey("isFinished")
            }
        }
    
        /// Complete the operation
        ///
        /// This will result in the appropriate KVN of isFinished and isExecuting
    
        public func completeOperation() {
            if executing {
                executing = false
                finished = true
            }
        }
    
        override public func start() {
            if cancelled {
                finished = true
                return
            }
    
            executing = true
    
            main()
        }
    }
    
    // this locking technique taken from "Advanced NSOperations", WWDC 2015
    // https://developer.apple.com/videos/play/wwdc2015/226/
    
    extension NSLock {
        func withCriticalScope<T>(@noescape block: Void -> T) -> T {
            lock()
            let value = block()
            unlock()
            return value
        }
    }
    

    完成后,您可以发起一系列可以按顺序执行的请求:

    let queue = NSOperationQueue()
    queue.maxConcurrentOperationCount = 1 
    
    for urlString in urlStrings {
        let url = NSURL(string: urlString)!
        print("queuing \(url.lastPathComponent)")
        let operation = NetworkOperation(url: url) { data, response, error in
            // do something with the `data`
        }
        queue.addOperation(operation)
    }
    

    或者,如果您不想遭受顺序请求的显着性能损失,但仍想限制并发程度(以最小化系统资源,避免超时等),您可以将maxConcurrentOperationCount 设置为像 3 或 4 这样的值。

    或者,您可以使用依赖项,例如在所有异步下载完成时触发某个进程:

    let queue = NSOperationQueue()
    queue.maxConcurrentOperationCount = 3
    
    let completionOperation = NSBlockOperation() {
        self.tableView.reloadData()
    }
    
    for urlString in urlStrings {
        let url = NSURL(string: urlString)!
        print("queuing \(url.lastPathComponent)")
        let operation = NetworkOperation(url: url) { data, response, error in
            // do something with the `data`
        }
        queue.addOperation(operation)
        completionOperation.addDependency(operation)
    }
    
    // now that they're all queued, you can queue the completion operation on the main queue, which will only start once the requests are done
    
    NSOperationQueue.mainQueue().addOperation(completionOperation)
    

    如果你想取消请求,你可以很容易地取消它们:

    queue.cancelAllOperations()
    

    操作是控制一系列异步任务的极其丰富的机制。如果您参考 WWDC 2015 视频 Advanced NSOperations,他们已经通过条件和观察者将这种模式提升到了一个全新的水平(尽管他们的解决方案对于简单的问题可能有点过度设计。恕我直言)。

    【讨论】:

      【解决方案4】:

      这是我已经在另一个类似问题的答案中建议的方法,专门针对您的问题量身定制:

      您的方法method1method2 都是异步的。异步函数应该有一种向调用者发出完成信号的方法。一种方法是使用完成处理程序:

          func method1(url: NSURL, completion: (Result1?, ErrorType?) -> ()) 
      
      
          func method2(url: NSURL), completion: (Result2?, ErrorType?) -> ())
      

      这里,Result1Result2 是异步函数的计算结果。由于任务可能会失败,因此完成处理程序的签名可以返回计算值或错误。

      假设,您的第一个方法 method1 评估一个项目列表,每个项目都包含另一个 URL。对于此列表中的每个 URL,您需要调用 method2

      将这些组合任务包装成一个新函数method(它也是异步的,因此它也有一个完成处理程序!):

      func method(completion: (Result?, ErrorType?)-> ()) {
          let url = ...
          self.method1(url) { (result1, error) in
              if let result = result1 {
                   // `result` is an array of items which have 
                   // a url as property:
                   let urls = result.map { $0.imageUrl } 
                   // Now, for each url, call method2:
                   // Use a dispatch group in order to signal
                   // completion of a group of asynchronous tasks
                   let group = dispatch_group_create()
                   let finalResult: SomeResult?
                   let finalError: ErrorType?
                   urls.forEach { imageUrl in 
                       dispatch_group_enter(group)
                       self.method2(imageUrl) { (result2, error) in
                           if let result = result2 {
                           } else {
                               // handle error (maybe set finalError and break)
                           }
                           dispatch_group_leave(group)
                       }
                   }
                   dispatch_group_notify(dispatch_get_global_queue(0,0)) {
                       completion(finalResult, finalError)
                   }
              } else {
                  // Always ensure the completion handler will be
                  // eventually called:
                  completion(nil, error)
              }
          }
      }
      

      上述方法使用调度组来对多个任务进行分组。当一个任务开始时,任务组的数量将使用dispatch_enter 增加。当一个任务完成时,组中的任务数量将减少为dispatch_group_leave

      当组为空时(所有任务都已完成),使用dispatch_group_notify 提交的块将在给定队列上执行。我们使用这个块来调用外部函数method的完成处理程序。

      您可以创造性地处理错误。例如,您可能只想忽略第二种方法 method2 的失败并继续获得结果,或者您可能希望取消所有仍在运行的任务并返回错误。您还可以在调用method2 时允许成功和失败,并将“结果”数组组合为finalResult,让组成功并返回finalResult - 它维护了每个调用的详细结果。

      您可能已经注意到,没有办法取消任务。是的,没有。这将需要 可取消 任务。这个问题也有优雅的解决方案,但这超出了这个答案。

      【讨论】:

        【解决方案5】:
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-03-27
        • 1970-01-01
        • 2021-05-26
        • 2015-09-24
        • 2021-01-11
        • 1970-01-01
        • 2020-03-14
        相关资源
        最近更新 更多