swift concurrency nsurlsession alamofire nsurlsessionconfiguration

swift - NSURLSession solicitudes concurrentes con Alamofire



concurrency nsurlsessionconfiguration (1)

Estoy experimentando un comportamiento extraño con mi aplicación de prueba. Tengo alrededor de 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, verificadas con Charles HTTP Proxy). Sin embargo, lo extraño es que todas las solicitudes que no finalizaron dentro de los 30 segundos posteriores a 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:

¿Es este un comportamiento esperado y cómo puedo asegurarme de que las solicitudes no se agoten antes de que se envíen?

¡Muchas gracias!


Sí, este es el comportamiento esperado. Una solución es ajustar sus solicitudes en una subclase NSOperation asíncrona NSOperation y luego usar 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 NSURLSession de NSURLSession nunca hizo esto, ni Alamofire lo hace.

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 quiero iniciar mis 50 solicitudes, haría 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, maxConcurrentOperationCount esas solicitudes, y no tenemos que preocuparnos por el tiempo de espera de ninguna de las solicitudes.

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

// // 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<T>( block: (Void) -> T) -> T { lock() let value = block() unlock() return value } }

Hay otras posibles variaciones de este patrón, pero solo asegúrese de que (a) devuelva true para asynchronous ; y (b) publique el isFinished y isExecuting necesario como se describe en la sección Configuración de operaciones para ejecución concurrente de la Guía de programación de concurrencia: Colas de operaciones .