NSURLSession concurrent requests with Alamofire

19,047

Yes, this is expected behavior. One solution is to wrap your requests in custom, asynchronous NSOperation subclass, and then use the maxConcurrentOperationCount of the operation queue to control the number of concurrent requests rather than the HTTPMaximumConnectionsPerHost parameter.

The original AFNetworking did a wonderful job wrapping the requests in operations, which made this trivial. But AFNetworking's NSURLSession implementation never did this, nor does Alamofire.


You can easily wrap the Request in an NSOperation subclass. For example:

class NetworkOperation: AsynchronousOperation {

    // define properties to hold everything that you'll supply when you instantiate
    // this object and will be used when the request finally starts
    //
    // in this example, I'll keep track of (a) URL; and (b) closure to call when request is done

    private let urlString: String
    private var networkOperationCompletionHandler: ((_ responseObject: Any?, _ error: Error?) -> Void)?

    // we'll also keep track of the resulting request operation in case we need to cancel it later

    weak var request: Alamofire.Request?

    // define init method that captures all of the properties to be used when issuing the request

    init(urlString: String, networkOperationCompletionHandler: ((_ responseObject: Any?, _ error: Error?) -> Void)? = nil) {
        self.urlString = urlString
        self.networkOperationCompletionHandler = networkOperationCompletionHandler
        super.init()
    }

    // when the operation actually starts, this is the method that will be called

    override func main() {
        request = Alamofire.request(urlString, method: .get, parameters: ["foo" : "bar"])
            .responseJSON { response in
                // do whatever you want here; personally, I'll just all the completion handler that was passed to me in `init`

                self.networkOperationCompletionHandler?(response.result.value, response.result.error)
                self.networkOperationCompletionHandler = nil

                // now that I'm done, complete this operation

                self.completeOperation()
        }
    }

    // we'll also support canceling the request, in case we need it

    override func cancel() {
        request?.cancel()
        super.cancel()
    }
}

Then, when I want to initiate my 50 requests, I'd do something like this:

let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2

for i in 0 ..< 50 {
    let operation = NetworkOperation(urlString: "http://example.com/request.php?value=\(i)") { responseObject, error in
        guard let responseObject = responseObject else {
            // handle error here

            print("failed: \(error?.localizedDescription ?? "Unknown error")")
            return
        }

        // update UI to reflect the `responseObject` finished successfully

        print("responseObject=\(responseObject)")
    }
    queue.addOperation(operation)
}

That way, those requests will be constrained by the maxConcurrentOperationCount, and we don't have to worry about any of the requests timing out..

This is an example AsynchronousOperation base class, which takes care of the KVN associated with asynchronous/concurrent NSOperation subclass:

//
//  AsynchronousOperation.swift
//
//  Created by Robert Ryan on 9/20/14.
//  Copyright (c) 2014 Robert Ryan. All rights reserved.
//

import Foundation

/// 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 : Operation {

    private let stateLock = NSLock()

    private var _executing: Bool = false
    override private(set) public var isExecuting: Bool {
        get {
            return stateLock.withCriticalScope { _executing }
        }
        set {
            willChangeValue(forKey: "isExecuting")
            stateLock.withCriticalScope { _executing = newValue }
            didChangeValue(forKey: "isExecuting")
        }
    }

    private var _finished: Bool = false
    override private(set) public var isFinished: Bool {
        get {
            return stateLock.withCriticalScope { _finished }
        }
        set {
            willChangeValue(forKey: "isFinished")
            stateLock.withCriticalScope { _finished = newValue }
            didChangeValue(forKey: "isFinished")
        }
    }

    /// Complete the operation
    ///
    /// This will result in the appropriate KVN of isFinished and isExecuting

    public func completeOperation() {
        if isExecuting {
            isExecuting = false
        }

        if !isFinished {
            isFinished = true
        }
    }

    override public func start() {
        if isCancelled {
            isFinished = true
            return
        }

        isExecuting = true

        main()
    }

    override public func main() {
        fatalError("subclasses must override `main`")
    }
}

/*
 Abstract:
 An extension to `NSLocking` to simplify executing critical code.

 Adapted from Advanced NSOperations sample code in WWDC 2015 https://developer.apple.com/videos/play/wwdc2015/226/
 Adapted from https://developer.apple.com/sample-code/wwdc/2015/downloads/Advanced-NSOperations.zip
 */

import Foundation

extension NSLocking {

    /// Perform closure within lock.
    ///
    /// An extension to `NSLocking` to simplify executing critical code.
    ///
    /// - parameter block: The closure to be performed.

    func withCriticalScope<T>(block: () throws -> T) rethrows -> T {
        lock()
        defer { unlock() }
        return try block()
    }
}

There are other possible variations of this pattern, but just ensure that you (a) return true for asynchronous; and (b) you post the necessary isFinished and isExecuting KVN as outlined the Configuring Operations for Concurrent Execution section of the Concurrency Programming Guide: Operation Queues.

Share:
19,047
Hannes
Author by

Hannes

UX &amp; Development

Updated on June 21, 2022

Comments

  • Hannes
    Hannes almost 2 years

    I'm experiencing some strange behaviour with my test app. I've about 50 simultaneous GET requests that I send to the same server. The server is an embedded server on a small piece of hardware with very limited resources. In order to optimize the performance for each single request, I configure one instance of Alamofire.Manager as follows:

    let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
    configuration.HTTPMaximumConnectionsPerHost = 2
    configuration.timeoutIntervalForRequest = 30
    let manager = Alamofire.Manager(configuration: configuration)
    

    When I send the requests with manager.request(...) they get dispatched in pairs of 2 (as expected, checked with Charles HTTP Proxy). The weird thing though is, that all requests which didn't finish within 30 seconds from the first request, get cancelled because of the timeout at the same time (even if they haven't been sent yet). Here is an illustration showcasing the behaviour:

    concurrent request illustration

    Is this an expected behaviour and how can I make sure that the requests won't get the timeout before they are even sent?

    Thanks a lot!

  • Hannes
    Hannes over 9 years
    Wow, thanks a lot Rob, doesn't happen that often to get such a great answer! Works like a charm.
  • Walter Martin Vargas-Pena
    Walter Martin Vargas-Pena about 8 years
    Question ConcurrentOperation is a subclass of what?
  • oroom
    oroom about 7 years
    When you add an operation to an operation queue, the queue ignores the value of the isAsynchronous property and always calls the start() method from a separate thread. Therefore, if you always run operations by adding them to an operation queue, there is no reason to make them asynchronous.
  • Rob
    Rob about 7 years
    @DzmitryNavak - Not true. Yes, the way the operation starts is similar, but the meaningful question is how the operation completes. If you don't make the operation isAsynchronous, the operation will complete immediately after the network request is started, thereby screwing up operation dependencies, maxConcurrentOperationCount, etc. But if you (a) set isAsynchronous and (b) do the necessary KVO, the operation won't be completed until you explicitly set isFinished to true and isExecuting to false (i.e. you can defer the completion of the operation until the network request is done).
  • Johan Levin
    Johan Levin almost 7 years
    @Rob, it's says "Copyright" and "All rights reserved". What's up with that? Is the code free to use?
  • Rob
    Rob almost 7 years
    All user contributions on Stack Overflow are contributed with cc by-sa 3.0 with attribution required. See the links at the bottom of this web page, in the footer. Bottom line, authors retain copyrights of contributions to Stack Overflow, but we also grant a perpetual license to use these specific contributions for any purpose(s), including commercial, with the sole requirements that (a) attribution is required and (b) that you will sharealike. In short, yes, it's free to use.
  • Johan Levin
    Johan Levin almost 7 years
    OK. CC BY-SA doesn't allow the code to be used in a closed source project, right?
  • Rob
    Rob almost 7 years
    That's not for me to say. Ask your team lead or legal counsel. I'd suggest researching existing Meta discussions (e.g. meta.stackoverflow.com/questions/350782/…) and direct any future questions there. BTW, I may have mispoken: It looks like non-code contributions are CC BY-SA, but that code contributions are covered by the MIT license.
  • JAHelia
    JAHelia over 6 years
    @Rob in the main function of NetworkOperation class you have to set [unowned self] or [weak self] in main() inside the completion handler in order to prevent memory leaks when you call self. self.networkOperationCompletionHandler
  • Rob
    Rob over 6 years
    @JAHelia - No I don't, because my AsynchronousOperation sets the closure to nil after performing it, thereby resolving any strong reference cycles.
  • famfamfam
    famfamfam over 5 years
    Hello. I saw u set 'queue.maxConcurrentOperationCount = 2', but you did called 50 times request, can u explain it?
  • famfamfam
    famfamfam over 5 years
    then can you provide the way to catch end of the last request?
  • Rob
    Rob over 5 years
    @famfamfam - In this asynchronous operation pattern, you create new operation that does what you need after this operation is done and make it dependent upon all of the individual operations of the individual network requests.
  • Rob
    Rob over 5 years
    @famfamfam "I saw u set maxConcurrentOperationCount = 2, but you did called 50 times request ..." That's the whole point: The OP wanted to queue up 50 requests, but never have more than two running simultaneously at the same time. The maxConcurrentOperationCount simply dictates how many can run at any given time. (You don't want too many running simultaneously because (a) URLSession can only run so many at a time, anyway, so you risk having latter requests time out; and (b) memory implications.) The above achieves a controlled degree of concurrency when queuing up many requests.