NSURLSesión de solicitudes concurrentes con Alamofire

Estoy experimentando un comportamiento extraño con mi aplicación de prueba. Tengo aproximadamente 50 solicitudes GET simultáneas que envío al mismo servidor. El servidor es un servidor integrado en una pequeña pieza de hardware con recursos muy limitados. Para optimizar el rendimiento de cada solicitud individual, configuro una instancia de Alamofire.Manager siguiente manera:

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

Cuando envío las solicitudes con manager.request(...) se envían en pares de 2 (como se esperaba, se comprueba con Charles HTTP Proxy). Lo extraño es que todas las solicitudes que no finalizaron en 30 segundos desde la primera solicitud se cancelan debido al tiempo de espera al mismo tiempo (incluso si aún no se han enviado). Aquí hay una ilustración que muestra el comportamiento:

Ilustración de solicitud concurrente

¿Es esto un comportamiento esperado y cómo puedo asegurarme de que las solicitudes no tengan el tiempo de espera antes de que se envíen?

¡Muchas gracias!

Sí, este es el comportamiento esperado. Una solución es envolver sus solicitudes en la subclase de NSOperation asíncrona NSOperation y luego utilizar maxConcurrentOperationCount de la cola de operaciones para controlar el número de solicitudes simultáneas en lugar del parámetro HTTPMaximumConnectionsPerHost .

El AFNetworking original hizo un trabajo maravilloso envolviendo las solicitudes en las operaciones, lo que hizo que esto fuera trivial. Pero la implementación de NSURLSession de NSURLSession nunca lo hizo, ni tampoco Alamofire.


Puede ajustar fácilmente la Request en una subclase de NSOperation . Por ejemplo:

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

Luego, cuando quiera iniciar mis 50 solicitudes, haré algo como esto:

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

De esa forma, esas solicitudes estarán limitadas por maxConcurrentOperationCount , y no tenemos que preocuparnos por que ninguna de las solicitudes se agote.

Este es un ejemplo de clase base AsynchronousOperation , que se encarga de la KVN asociada con la subclase asíncrona / simultánea NSOperation :

 // // 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`") } } /* Copyright (C) 2015 Apple Inc. All Rights Reserved. See LICENSE.txt for this sample's licensing information Abstract: An extension to `NSLock` to simplify executing critical code. From Advanced NSOperations sample code in WWDC 2015 https://developer.apple.com/videos/play/wwdc2015/226/ From https://developer.apple.com/sample-code/wwdc/2015/downloads/Advanced-NSOperations.zip */ import Foundation extension NSLock { /// Perform closure within lock. /// /// An extension to `NSLock` to simplify executing critical code. /// /// - parameter block: The closure to be performed. func withCriticalScope( block: (Void) -> T) -> T { lock() let value = block() unlock() return value } } 

Hay otras variaciones posibles de este patrón, pero solo asegúrese de que (a) devuelva true para asynchronous ; y (b) usted publica el necesario es isFinished y está isExecuting KVN como se describe en la sección Configuración de operaciones para la ejecución simultánea de la Guía de progtwigción de concurrencia: Colas de operación .