Espere hasta que el bucle rápido con solicitudes de red asincrónicas termine de ejecutarse

159

Me gustaría un for in loop para enviar un montón de solicitudes de red a firebase, luego pasar los datos a un nuevo controlador de vista una vez que el método termine de ejecutarse. Aquí está mi código:

var datesArray = [String: AnyObject]()

for key in locationsArray {       
    let ref = Firebase(url: "http://myfirebase.com/" + "\(key.0)")
    ref.observeSingleEventOfType(.Value, withBlock: { snapshot in

        datesArray["\(key.0)"] = snapshot.value
    })
}
// Segue to new view controller here and pass datesArray once it is complete 

Tengo un par de preocupaciones Primero, ¿cómo espero hasta que finalice el ciclo for y se completen todas las solicitudes de red? No puedo modificar la función observeSingleEventOfType, es parte del SDK de Firebase. Además, ¿crearé algún tipo de condición de carrera al intentar acceder a la matriz de fechas desde diferentes iteraciones del ciclo for (espero que tenga sentido)? He estado leyendo sobre GCD y NSOperation pero estoy un poco perdido ya que esta es la primera aplicación que he creado.

Nota: La matriz de ubicaciones es una matriz que contiene las claves a las que necesito acceder en firebase. Además, es importante que las solicitudes de red se disparen de forma asincrónica. Solo quiero esperar hasta que se completen TODAS las solicitudes asincrónicas antes de pasar el arreglo de fechas al siguiente controlador de vista.

Josh
fuente

Respuestas:

338

Puede usar grupos de despacho para disparar una devolución de llamada asíncrona cuando finalicen todas sus solicitudes.

Aquí hay un ejemplo usando grupos de despacho para ejecutar una devolución de llamada de forma asincrónica cuando todas las solicitudes de red han finalizado.

override func viewDidLoad() {
    super.viewDidLoad()

    let myGroup = DispatchGroup()

    for i in 0 ..< 5 {
        myGroup.enter()

        Alamofire.request("https://httpbin.org/get", parameters: ["foo": "bar"]).responseJSON { response in
            print("Finished request \(i)")
            myGroup.leave()
        }
    }

    myGroup.notify(queue: .main) {
        print("Finished all requests.")
    }
}

Salida

Finished request 1
Finished request 0
Finished request 2
Finished request 3
Finished request 4
Finished all requests.
paulvs
fuente
Esto funcionó muy bien! ¡Gracias! ¿Tienes alguna idea de si voy a encontrarme con alguna condición de carrera cuando intento actualizar las fechas?
Josh
No creo que haya una condición de carrera aquí porque todas las solicitudes agregan valores al datesArrayuso de una clave diferente.
paulvs
1
@ Josh Con respecto a la condición de carrera: se produce una condición de carrera, si se accede a la misma ubicación de memoria desde diferentes subprocesos, donde al menos un acceso es una escritura, sin usar sincronización. Sin embargo, todos los accesos dentro de la misma cola de despacho en serie están sincronizados. La sincronización también ocurre con las operaciones de memoria que ocurren en la cola de despacho A, que se envía a otra cola de despacho B. Todas las operaciones en la cola A se sincronizan en la cola B. Entonces, si observa la solución, no se garantiza automáticamente que los accesos estén sincronizados. ;)
CouchDeveloper
@josh, tenga en cuenta que la "programación de la pista de carreras" es, en una palabra, estupendamente difícil. Nunca es posible decir instantáneamente "usted tiene / no tiene un problema allí". Para los programadores aficionados: "simplemente" siempre funciona de una manera que significa que los problemas de la pista son, simplemente, imposibles. (Por ejemplo, cosas como "solo hacer una cosa a la vez", etc.) Incluso hacerlo es un gran desafío de programación.
Fattie
Super guay. Pero tengo una pregunta. Suponga que la solicitud 3 y la solicitud 4 fallaron (por ejemplo, error del servidor, error de autorización, cualquier cosa), entonces, ¿cómo llamar al bucle nuevamente para solo las solicitudes restantes (solicitud 3 y solicitud 4)?
JD.
43

Xcode 8.3.1 - Swift 3

Esta es la respuesta aceptada de paulvs, convertida a Swift 3:

let myGroup = DispatchGroup()

override func viewDidLoad() {
    super.viewDidLoad()

    for i in 0 ..< 5 {
        myGroup.enter()
        Alamofire.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"]).responseJSON { response in
            print("Finished request \(i)")
            myGroup.leave()
        }
    }

    myGroup.notify(queue: DispatchQueue.main, execute: {
        print("Finished all requests.")
    })
}
Canal
fuente
1
Hola, ¿esto funciona por digamos 100 solicitudes? o 1000? Porque estoy tratando de hacer esto con aproximadamente 100 solicitudes y me estoy bloqueando al completar la solicitud.
lopes710
I second @ lopes710-- Esto parece permitir que todas las solicitudes operen en paralelo, ¿verdad?
Chris Prince
si tengo 2 solicitudes de red, una anidada con la otra, dentro de un bucle for, entonces cómo asegurarme de que para cada iteración del bucle for, ambas solicitudes se hayan completado. ?
Awais Fayyaz
@Channel, por favor, ¿hay alguna manera de que pueda ordenar esto?
Israel Meshileya
41

Swift 3 o 4

Si no te importan las órdenes , usa la respuesta de @paulvs , funciona perfectamente.

de lo contrario, en caso de que alguien quiera ordenar el resultado en lugar de dispararlo al mismo tiempo, aquí está el código.

let dispatchGroup = DispatchGroup()
let dispatchQueue = DispatchQueue(label: "any-label-name")
let dispatchSemaphore = DispatchSemaphore(value: 0)

dispatchQueue.async {

    // use array categories as an example.
    for c in self.categories {

        if let id = c.categoryId {

            dispatchGroup.enter()

            self.downloadProductsByCategory(categoryId: id) { success, data in

                if success, let products = data {

                    self.products.append(products)
                }

                dispatchSemaphore.signal()
                dispatchGroup.leave()
            }

            dispatchSemaphore.wait()
        }
    }
}

dispatchGroup.notify(queue: dispatchQueue) {

    DispatchQueue.main.async {

        self.refreshOrderTable { _ in

            self.productCollectionView.reloadData()
        }
    }
}
Eterno
fuente
Mi aplicación tiene que enviar varios archivos a un servidor FTP, que también incluye iniciar sesión primero. Este enfoque garantiza que la aplicación solo inicie sesión una vez (antes de cargar el primer archivo), en lugar de intentar hacerlo varias veces, todo básicamente al mismo tiempo (como con el enfoque "desordenado"), lo que provocaría errores. ¡Gracias!
Neph
Sin embargo, tengo una pregunta: ¿importa si lo haces dispatchSemaphore.signal()antes o después de dejar el dispatchGroup? Se podría pensar que es mejor desbloquear el semáforo lo más tarde posible, pero no estoy seguro de si dejar el grupo interfiere con eso. Probé ambas órdenes y no pareció hacer la diferencia.
Neph
16

Detalles

  • Xcode 10.2.1 (10E1001), Swift 5

Solución

import Foundation

class SimultaneousOperationsQueue {
    typealias CompleteClosure = ()->()

    private let dispatchQueue: DispatchQueue
    private lazy var tasksCompletionQueue = DispatchQueue.main
    private let semaphore: DispatchSemaphore
    var whenCompleteAll: (()->())?
    private lazy var numberOfPendingActionsSemaphore = DispatchSemaphore(value: 1)
    private lazy var _numberOfPendingActions = 0

    var numberOfPendingTasks: Int {
        get {
            numberOfPendingActionsSemaphore.wait()
            defer { numberOfPendingActionsSemaphore.signal() }
            return _numberOfPendingActions
        }
        set(value) {
            numberOfPendingActionsSemaphore.wait()
            defer { numberOfPendingActionsSemaphore.signal() }
            _numberOfPendingActions = value
        }
    }

    init(numberOfSimultaneousActions: Int, dispatchQueueLabel: String) {
        dispatchQueue = DispatchQueue(label: dispatchQueueLabel)
        semaphore = DispatchSemaphore(value: numberOfSimultaneousActions)
    }

    func run(closure: ((@escaping CompleteClosure) -> Void)?) {
        numberOfPendingTasks += 1
        dispatchQueue.async { [weak self] in
            guard   let self = self,
                    let closure = closure else { return }
            self.semaphore.wait()
            closure {
                defer { self.semaphore.signal() }
                self.numberOfPendingTasks -= 1
                if self.numberOfPendingTasks == 0, let closure = self.whenCompleteAll {
                    self.tasksCompletionQueue.async { closure() }
                }
            }
        }
    }

    func run(closure: (() -> Void)?) {
        numberOfPendingTasks += 1
        dispatchQueue.async { [weak self] in
            guard   let self = self,
                    let closure = closure else { return }
            self.semaphore.wait(); defer { self.semaphore.signal() }
            closure()
            self.numberOfPendingTasks -= 1
            if self.numberOfPendingTasks == 0, let closure = self.whenCompleteAll {
                self.tasksCompletionQueue.async { closure() }
            }
        }
    }
}

Uso

let queue = SimultaneousOperationsQueue(numberOfSimultaneousActions: 1, dispatchQueueLabel: "AnyString")
queue.whenCompleteAll = { print("All Done") }

 // add task with sync/async code
queue.run { completeClosure in
    // your code here...

    // Make signal that this closure finished
    completeClosure()
}

 // add task only with sync code
queue.run {
    // your code here...
}

Muestra completa

import UIKit

class ViewController: UIViewController {

    private lazy var queue = { SimultaneousOperationsQueue(numberOfSimultaneousActions: 1,
                                                           dispatchQueueLabel: "AnyString") }()
    private weak var button: UIButton!
    private weak var label: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        let button = UIButton(frame: CGRect(x: 50, y: 80, width: 100, height: 100))
        button.setTitleColor(.blue, for: .normal)
        button.titleLabel?.numberOfLines = 0
        view.addSubview(button)
        self.button = button

        let label = UILabel(frame: CGRect(x: 180, y: 50, width: 100, height: 100))
        label.text = ""
        label.numberOfLines = 0
        label.textAlignment = .natural
        view.addSubview(label)
        self.label = label

        queue.whenCompleteAll = { [weak self] in self?.label.text = "All tasks completed" }

        //sample1()
        sample2()
    }

    func sample1() {
        button.setTitle("Run 2 task", for: .normal)
        button.addTarget(self, action: #selector(sample1Action), for: .touchUpInside)
    }

    func sample2() {
        button.setTitle("Run 10 tasks", for: .normal)
        button.addTarget(self, action: #selector(sample2Action), for: .touchUpInside)
    }

    private func add2Tasks() {
        queue.run { completeTask in
            DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + .seconds(1)) {
                DispatchQueue.main.async { [weak self] in
                    guard let self = self else { return }
                    self.label.text = "pending tasks \(self.queue.numberOfPendingTasks)"
                }
                completeTask()
            }
        }
        queue.run {
            sleep(1)
            DispatchQueue.main.async { [weak self] in
                guard let self = self else { return }
                self.label.text = "pending tasks \(self.queue.numberOfPendingTasks)"
            }
        }
    }

    @objc func sample1Action() {
        label.text = "pending tasks \(queue.numberOfPendingTasks)"
        add2Tasks()
    }

    @objc func sample2Action() {
        label.text = "pending tasks \(queue.numberOfPendingTasks)"
        for _ in 0..<5 { add2Tasks() }
    }
}
Vasily Bodnarchuk
fuente
5

Necesitará usar semáforos para este propósito.

 //Create the semaphore with count equal to the number of requests that will be made.
let semaphore = dispatch_semaphore_create(locationsArray.count)

        for key in locationsArray {       
            let ref = Firebase(url: "http://myfirebase.com/" + "\(key.0)")
            ref.observeSingleEventOfType(.Value, withBlock: { snapshot in

                datesArray["\(key.0)"] = snapshot.value

               //For each request completed, signal the semaphore
               dispatch_semaphore_signal(semaphore)


            })
        }

       //Wait on the semaphore until all requests are completed
      let timeoutLengthInNanoSeconds: Int64 = 10000000000  //Adjust the timeout to suit your case
      let timeout = dispatch_time(DISPATCH_TIME_NOW, timeoutLengthInNanoSeconds)

      dispatch_semaphore_wait(semaphore, timeout)

     //When you reach here all request would have been completed or timeout would have occurred.
Shripada
fuente
3

Swift 3: también puede usar semáforos de esta manera. Resulta muy útil, además de que puede realizar un seguimiento exacto de cuándo y qué procesos se completan. Esto ha sido extraído de mi código:

    //You have to create your own queue or if you need the Default queue
    let persons = persistentContainer.viewContext.persons
    print("How many persons on database: \(persons.count())")
    let numberOfPersons = persons.count()

    for eachPerson in persons{
        queuePersonDetail.async {
            self.getPersonDetailAndSave(personId: eachPerson.personId){person2, error in
                print("Person detail: \(person2?.fullName)")
                //When we get the completionHandler we send the signal
                semaphorePersonDetailAndSave.signal()
            }
        }
    }

    //Here we will wait
    for i in 0..<numberOfPersons{
        semaphorePersonDetailAndSave.wait()
        NSLog("\(i + 1)/\(persons.count()) completed")
    }
    //And here the flow continues...
freaklix
fuente
1

Podemos hacer esto con recursividad. Obtenga una idea del siguiente código:

var count = 0

func uploadImages(){

    if count < viewModel.uploadImageModelArray.count {
        let item = viewModel.uploadImageModelArray[count]
        self.viewModel.uploadImageExpense(filePath: item.imagePath, docType: "image/png", fileName: item.fileName ?? "", title: item.imageName ?? "", notes: item.notes ?? "", location: item.location ?? "") { (status) in

            if status ?? false {
                // successfully uploaded
            }else{
                // failed
            }
            self.count += 1
            self.uploadImages()
        }
    }
}
Profundo
fuente
-1

El grupo de despacho es bueno pero el orden de las solicitudes enviadas es aleatorio.

Finished request 1
Finished request 0
Finished request 2

En el caso de mi proyecto, cada solicitud que se necesita para iniciar es el orden correcto. Si esto podría ayudar a alguien:

public class RequestItem: NSObject {
    public var urlToCall: String = ""
    public var method: HTTPMethod = .get
    public var params: [String: String] = [:]
    public var headers: [String: String] = [:]
}


public func trySendRequestsNotSent (trySendRequestsNotSentCompletionHandler: @escaping ([Error]) -> () = { _ in }) {

    // If there is requests
    if !requestItemsToSend.isEmpty {
        let requestItemsToSendCopy = requestItemsToSend

        NSLog("Send list started")
        launchRequestsInOrder(requestItemsToSendCopy, 0, [], launchRequestsInOrderCompletionBlock: { index, errors in
            trySendRequestsNotSentCompletionHandler(errors)
        })
    }
    else {
        trySendRequestsNotSentCompletionHandler([])
    }
}

private func launchRequestsInOrder (_ requestItemsToSend: [RequestItem], _ index: Int, _ errors: [Error], launchRequestsInOrderCompletionBlock: @escaping (_ index: Int, _ errors: [Error] ) -> Void) {

    executeRequest(requestItemsToSend, index, errors, executeRequestCompletionBlock: { currentIndex, errors in
        if currentIndex < requestItemsToSend.count {
            // We didn't reach last request, launch next request
            self.launchRequestsInOrder(requestItemsToSend, currentIndex, errors, launchRequestsInOrderCompletionBlock: { index, errors in

                launchRequestsInOrderCompletionBlock(currentIndex, errors)
            })
        }
        else {
            // We parse and send all requests
            NSLog("Send list finished")
            launchRequestsInOrderCompletionBlock(currentIndex, errors)
        }
    })
}

private func executeRequest (_ requestItemsToSend: [RequestItem], _ index: Int, _ errors: [Error], executeRequestCompletionBlock: @escaping (_ index: Int, _ errors: [Error]) -> Void) {
    NSLog("Send request %d", index)
    Alamofire.request(requestItemsToSend[index].urlToCall, method: requestItemsToSend[index].method, parameters: requestItemsToSend[index].params, headers: requestItemsToSend[index].headers).responseJSON { response in

        var errors: [Error] = errors
        switch response.result {
        case .success:
            // Request sended successfully, we can remove it from not sended request array
            self.requestItemsToSend.remove(at: index)
            break
        case .failure:
            // Still not send we append arror
            errors.append(response.result.error!)
            break
        }
        NSLog("Receive request %d", index)
        executeRequestCompletionBlock(index+1, errors)
    }
}

Llamada :

trySendRequestsNotSent()

Resultado:

Send list started
Send request 0
Receive request 0
Send request 1
Receive request 1
Send request 2
Receive request 2
...
Send list finished

Ver para más información: Gist

Aximem
fuente