How to make inner async request complete first bef

2019-07-23 19:02发布

I've been trying to achieve this for some time and cannot get it to work.

First let me show a simple sample code:

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

What I'm basically doing is I perform an asynchronous request within my methodOne, and within that function, I call my methodTwo which performs another asynchronous request.

The problem I'm having is that when methodTwo is called, it never enters the asynchronous session. It does however, within methodTwo, enter the asynchronous session, but only once k = someArray.count - 1. It's basically queuing up until the very end, which is not what I'd like to achieve.

Here's a sample output:

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

In other words, I'd like to have the async request from methodTwo complete on each iteration before the async request from methodOne completes.

Here's a sample output of what my goal is:

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

I've found something similar here: Wait until first async function is completed then execute the second async function

However, I couldn't get this to work with the suggestions and solutions.

Can someone point me in the right direction?

Thanks

5条回答
太酷不给撩
2楼-- · 2019-07-23 19:12

Instead of semaphores or groups that others have advised (which blocks a thread, which can be problematic if you have too many threads blocked), I would use a custom, asynchronous NSOperation subclass for network requests. Once you've wrapped the request in an asynchronous NSOperation, you can then add a bunch of operations to an operation queue, not blocking any threads, but enjoying dependencies between these asynchronous operations.

For example, a network operation might look like:

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

Having done that, you can initiate a whole series of requests that could be performed sequentially:

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

Or, if you don't want to suffer the significant performance penalty of sequential requests, but still want to constrain the degree of concurrency (to minimize system resources, avoid timeouts, etc), you can set maxConcurrentOperationCount to a value like 3 or 4.

Or, you can use dependencies, for example to trigger some process when all of the asynchronous downloads are done:

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)

And if you want to cancel the requests, you can easily cancel them:

queue.cancelAllOperations()

Operations are incredibly rich mechanism for controlling a series of asynchronous tasks. If you refer to WWDC 2015 video Advanced NSOperations, they have taken this pattern to a whole other level with conditions and observers (though their solution might be a bit overengineered for simple problems. IMHO).

查看更多
贪生不怕死
3楼-- · 2019-07-23 19:19
ゆ 、 Hurt°
4楼-- · 2019-07-23 19:26

You should use synchronous request. It's easy to use with this extension:

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

and send synchronous request in 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")
    }
}

Also you can manage it using Dispatch Queue. Learn more about GCD

查看更多
欢心
5楼-- · 2019-07-23 19:27

Here is the approach I already suggested in another answers for a simalar question, specifically tailored for your problem:

Your methods method1 and method2 are both asynchronous. Asynchronous functions should have a means to signal completion to the caller. One approach for this is using completion handlers:

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


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

Here, Result1 and Result2 is the computed result of the asynchronous functions. Since a task may fail, the signature of the completion handler has a means to either return the computed value or an error.

Suppose, your first method method1 evaluates a list of items, each containing another URL. For each URL in this list, you want to call method2.

Wrap these composed tasks into a new function method (it's also asynchronous, and thus it also has a completion handler as well!):

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

The above approach uses a dispatch group in order to group a number of tasks. When a task starts, the group number of tasks will be increased using dispatch_enter. When a task will be completed, the number of tasks in the group will be decreased with dispatch_group_leave.

When the group is empty (all tasks have completed) the block submitted with dispatch_group_notify will be executed on the given queue. We use this block in order to call the completion handler of the outer function method.

You can be creative regarding handling the errors. For example, you might want to just ignore a failure of second method method2 and continue to get a result, or you might want to cancel every task which is still running and return an error. You could also allow to have success and failure when calling method2 and compose an array of "result" as finalResult, let the group succeed and return finalResult - which maintains a detailed result about each call.

You may have noticed, that there is no means to cancel a task. Yes, there isn't. This would require cancelable tasks. There are elegant solutions for this problem too, but this beyond this answer.

查看更多
趁早两清
6楼-- · 2019-07-23 19:30

One way to do this is to change methodTwo() to accept a callback as an argument, then you can use a semaphore:

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

Note that to not block the delegate queue of methodOne's task callback, the example creates its own queue that you can block at will.

查看更多
登录 后发表回答