NSURLSession gleichzeitige Anfragen mit Alamofire

68

Ich habe ein seltsames Verhalten mit meiner Test-App. Ich habe ungefähr 50 gleichzeitige GET-Anforderungen, die ich an denselben Server sende. Der Server ist ein eingebetteter Server auf einer kleinen Hardware mit sehr begrenzten Ressourcen. Um die Leistung für jede einzelne Anforderung zu optimieren, konfiguriere ich eine Instanz Alamofire.Managerwie folgt:

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

Wenn ich die Anfragen mit sende, werden manager.request(...)sie in Zweierpaaren versandt (wie erwartet mit Charles HTTP Proxy überprüft). Das Seltsame ist jedoch, dass alle Anfragen, die nicht innerhalb von 30 Sekunden nach der ersten Anfrage abgeschlossen wurden, aufgrund des Zeitlimits gleichzeitig storniert werden (auch wenn sie noch nicht gesendet wurden). Hier ist eine Illustration, die das Verhalten zeigt:

Abbildung der gleichzeitigen Anforderung

Ist dies ein erwartetes Verhalten und wie kann ich sicherstellen, dass die Anforderungen nicht das Zeitlimit erhalten, bevor sie überhaupt gesendet werden?

Vielen Dank!

Hannes
quelle
Vielleicht möchten Sie tatsächlich "timeoutIntervalForResource , not timeoutIntervalForRequest " festlegen?
Mattt
Danke, aber ich habe beides versucht und das Gleiche passiert immer wieder.
Hannes
Ihre Methode funktioniert nicht mehr in Alamofire 4, bitte aktualisieren Sie es
famfamfam
Mit welchem ​​Programm haben Sie dieses Diagramm erstellt?
Nik Kov
@ NikKov Ich habe Photoshop verwendet
Hannes

Antworten:

125

Ja, dies ist das erwartete Verhalten. Eine Lösung besteht darin, Ihre Anforderungen in eine benutzerdefinierte, asynchrone NSOperationUnterklasse zu packen und dann mithilfe maxConcurrentOperationCountder Operationswarteschlange die Anzahl der gleichzeitigen Anforderungen anstelle der zu steuernHTTPMaximumConnectionsPerHost Parameters .

Das ursprüngliche AFNetworking hat die Anforderungen in Betrieb genommen, was dies trivial machte. Die NSURLSessionImplementierung von AFNetworking hat dies jedoch nie getan, ebenso wenig wie Alamofire.


Sie können das einfach Requestin eine NSOperationUnterklasse einschließen. Zum Beispiel:

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

Wenn ich dann meine 50 Anfragen initiieren möchte, würde ich so etwas tun:

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

Auf diese Weise werden diese Anforderungen durch die eingeschränkt maxConcurrentOperationCount , und wir müssen uns keine Sorgen machen, dass die Anforderungen abgelaufen sind.

Dies ist eine beispielhafte AsynchronousOperationBasisklasse, die sich um die KVN kümmert, die der asynchronen / gleichzeitigen NSOperationUnterklasse zugeordnet ist:

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

Es gibt andere mögliche Variationen dieses Musters, aber stellen Sie einfach sicher, dass Sie (a) zurückkehren truefür asynchronous; und (b) Sie veröffentlichen die erforderliche isFinishedund die isExecutingKVN wie im Abschnitt Konfigurieren von Vorgängen für die gleichzeitige Ausführung des Concurrency Programming Guide: Operation Queues beschrieben .

rauben
quelle
1
Wow, vielen Dank, Rob, kommt nicht so oft vor, um eine so gute Antwort zu bekommen! Klappt wunderbar.
Hannes
1
Alle Benutzerbeiträge zu Stack Overflow werden mit cc by-sa 3.0 bereitgestellt, wobei eine Zuordnung erforderlich ist . Siehe die Links am Ende dieser Webseite in der Fußzeile. Unter dem Strich behalten sich die Autoren das Urheberrecht an Beiträgen zu Stack Overflow vor, aber wir gewähren auch eine unbefristete Lizenz zur Nutzung dieser spezifischen Beiträge für alle Zwecke, einschließlich kommerzieller, mit den einzigen Anforderungen, dass (a) eine Zuordnung erforderlich ist und (b) dass Sie werden Sharealike. Kurz gesagt, ja, es ist kostenlos zu benutzen.
Rob
1
@JAHelia - Nein, das tue ich nicht, weil mein AsynchronousOperationden Abschluss nilnach dem Ausführen auf setzt und dadurch starke Referenzzyklen auflöst.
Rob
1
@famfamfam - In diesem asynchronen Operationsmuster erstellen Sie eine neue Operation, die das tut, was Sie nach Abschluss dieser Operation benötigen, und machen sie von allen einzelnen Operationen der einzelnen Netzwerkanforderungen abhängig.
Rob
1
@famfamfam "Ich habe gesehen, wie du gesetzt maxConcurrentOperationCount = 2hast, aber du hast 50 Mal angerufen ..." Das ist der springende Punkt: Das OP wollte 50 Anfragen in die Warteschlange stellen, aber nie mehr als zwei gleichzeitig laufen lassen. Das bestimmt maxConcurrentOperationCounteinfach, wie viele zu einem bestimmten Zeitpunkt ausgeführt werden können. (Sie möchten nicht, dass zu viele gleichzeitig ausgeführt werden, da (a) URLSessionohnehin nur so viele gleichzeitig ausgeführt werden können, sodass das Risiko besteht, dass letztere Anforderungen eine Zeitüberschreitung aufweisen, und (b) Auswirkungen auf den Speicher.) Mit dem oben genannten Verfahren wird ein kontrollierter Grad an Parallelität erreicht beim Anstehen vieler Anfragen.
Rob