如何在Swift中完成外部异步请求之前完成内部异步请求?

问题描述:

我一直试图达到这一段时间,无法让它工作。如何在Swift中完成外部异步请求之前完成内部异步请求?

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

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 

换句话说,我想有从methodTwo异步请求从methodOne完成异步请求之前每次迭代完成。

这里是我的目标是什么样的输出:

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

但是,我不能让这个与建议和解决方案的工作。

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

谢谢

+0

http://stackoverflow.com/a/28169498 – Shubhank

+0

如果您在MethodOne和MethodTwo之间有执行依赖关系的顺序,为什么您使用两个单独的异步操作? – TJA

+0

看看这个非常相似的问题和我最近给出的答案:http://stackoverflow.com/a/37155037/465677 – CouchDeveloper

一种方式做,这是改变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的任务回调的委托队列,示例将创建自己的队列,您可以随意阻止该队列。

+0

我不同意阻塞队列将罚款。实际上,你的方法会导致严重的问题:每次执行循环中的代码块时,它都会调度到队列中,并随后阻塞底层线程。下一次迭代需要一个新的线程,并因此产生另一个线程。由于GCD限制了线程的最大数量(类似于64),所以调用线程(主线程)将会阻塞,并且可能还会死锁。 – CouchDeveloper

+0

@CouchDeveloper是的,你是对的,显然数据任务总是调用“委托队列”。更新了示例以使用它自己的队列。 – Pascal

+0

我将你的建议应用于我的实际代码,但似乎由于某些代码的执行时间早于其他代码而出现一些错误...您是否可以从提供的链接查看示例代码? – Pangu

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

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") 
    } 
} 

您也可以使用调度队列管理。 Learn more about GCD

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

例如,网络操作可能是这样的:如果你

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) 
} 

或者:

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 
    } 
} 

已经这样做了,你可以启动一个全系列的请求,可以依次进行不希望受到连续请求的显着性能损失,但仍然希望限制并发程度(以最小化系统资源,避免超时等),可以将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,他们已经将这种模式带入了另一个层面,包括条件和观察者(虽然他们的解决方案对于简单的问题可能有点过分)。

这里是我已经在另一个答案的问题simalar,专门为您量身定制的问题建议的方法:

你的方法method1method2都是异步的。异步函数应该有一种方法来向调用者发送完成信号。这样做的一个方法是使用完成处理程序:

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


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

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

假设您的第一个方法method1评估项目列表,每个项目都包含另一个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 - 该维护关于每个调用的详细结果。

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