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
.