Совлокальные запросы NSURLSession с Alamofire

Я испытываю странное поведение с моим тестовым приложением. У меня около 50 одновременных запросов GET, которые я отправляю на тот же сервер. Сервер представляет собой встроенный сервер на небольшом аппаратном обеспечении с очень ограниченными ресурсами. Чтобы оптимизировать производительность для каждого отдельного запроса, я настраиваю один экземпляр Alamofire.Manager следующим образом:

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

Когда я отправляю запросы с помощью manager.request(...), они отправляются парами из 2 (как и ожидалось, проверяется с помощью HTTP-прокси Charles). Странно, однако, что все запросы, которые не завершились в течение 30 секунд с первого запроса, будут отменены из-за тайм-аута в одно и то же время (даже если они еще не были отправлены). Вот иллюстрация, демонстрирующая поведение:

concurrent request illustration

Является ли это ожидаемым поведением и как я могу убедиться, что запросы не получат таймаут, прежде чем они будут отправлены?

Спасибо большое!

Ответ 1

Да, это ожидаемое поведение. Одним из решений является обернуть ваши запросы в пользовательский асинхронный подкласс NSOperation, а затем использовать maxConcurrentOperationCount очереди операций для управления количеством параллельных запросов, а не параметром HTTPMaximumConnectionsPerHost.

Оригинальная AFNetworking сделала замечательную работу, завершая запросы в операциях, что сделало это тривиальным. Но реализация AFNetworking NSURLSession никогда не делала этого, и Alamofire.


Вы можете легко обернуть Request в подкласс NSOperation. Например:

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

Затем, когда я хочу инициировать мои 50 запросов, я бы сделал что-то вроде этого:

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

Таким образом, эти запросы будут ограничены maxConcurrentOperationCount, и нам не нужно беспокоиться ни о каком из тайм-аутов запросов.

Это пример базового класса AsynchronousOperation, который заботится о KVN, связанном с асинхронным/параллельным подклассом 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 {

    override public var isAsynchronous: Bool { return true }

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

Существуют и другие возможные варианты этого шаблона, но просто убедитесь, что вы (a) возвращаете true для asynchronous; и (b) вы публикуете необходимые isFinished и isExecuting KVN, как описано в разделе "Конфигурирование операций для параллельного выполнения" Concurrency Руководство по программированию: операция Очереди.