Attendere il completamento dell'esecuzione del ciclo rapido con richieste di rete asincrone


159

Vorrei un ciclo for in per inviare un gruppo di richieste di rete a Firebase, quindi passare i dati a un nuovo controller di visualizzazione una volta che il metodo termina l'esecuzione. Ecco il mio codice:

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 

Ho un paio di preoccupazioni. Innanzitutto, come posso attendere fino al termine del ciclo for e al completamento di tutte le richieste di rete? Non riesco a modificare la funzione observSingleEventOfType, fa parte dell'SDK Firebase. Inoltre, creerò una sorta di condizione di gara cercando di accedere alle dateArray da diverse iterazioni del ciclo for (speranza che abbia senso)? Ho letto di GCD e NSOperation, ma mi sono perso un po 'perché questa è la prima app che ho creato.

Nota: l'array Locations è un array che contiene le chiavi di cui ho bisogno per accedere a Firebase. Inoltre, è importante che le richieste di rete vengano attivate in modo asincrono. Voglio solo attendere il completamento di TUTTE le richieste asincrone prima di passare le dateArray al controller della vista successiva.

Risposte:


338

È possibile utilizzare i gruppi di invio per attivare un callback asincrono al termine di tutte le richieste.

Ecco un esempio che utilizza i gruppi di invio per eseguire un callback in modo asincrono quando tutte le richieste di rete sono state completate.

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

Produzione

Finished request 1
Finished request 0
Finished request 2
Finished request 3
Finished request 4
Finished all requests.

Questo ha funzionato alla grande! Grazie! Hai idea se mi imbatterò in condizioni di gara quando provo ad aggiornare le dateArray?
Josh

Non penso che ci sia una condizione di competizione qui perché tutte le richieste aggiungono valori datesArrayall'utilizzo di una chiave diversa.
Paul 10s16

1
@Josh Per quanto riguarda le condizioni di gara: si verifica una condizione di gara, se si accede alla stessa posizione di memoria da thread diversi, dove almeno un accesso è una scrittura - senza utilizzare la sincronizzazione. Tuttavia, tutti gli accessi all'interno della stessa coda di invio seriale sono sincronizzati. La sincronizzazione si verifica anche con le operazioni di memoria che si verificano nella coda di invio A, che invia a un'altra coda di invio B. Tutte le operazioni nella coda A vengono quindi sincronizzate nella coda B. Pertanto, se si osserva la soluzione, non è garantito automaticamente che gli accessi siano sincronizzati. ;)
CouchDeveloper

@josh, tieni presente che la "programmazione in pista" è, in una parola, incredibilmente difficile. Non è mai possibile dire all'istante "fai / non hai problemi lì". Per i programmatori di hobbisti: "semplicemente" funziona sempre in modo tale che i problemi in pista siano semplicemente impossibili. (Ad esempio, cose come "fare solo una cosa alla volta" ecc.) Anche farlo è un'enorme sfida di programmazione.
Fattie,

Super cool. Ma ho una domanda Supponiamo che la richiesta 3 e la richiesta 4 non siano riuscite (ad es. Errore del server, errore di autorizzazione, nulla), quindi come chiamare nuovamente il ciclo solo per le richieste rimanenti (richiesta 3 e richiesta 4)?
JD.

43

Xcode 8.3.1 - Swift 3

Questa è la risposta accettata da paulvs, convertita in 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.")
    })
}

1
Ciao, funziona per diciamo 100 richieste? o 1000? Perché sto provando a farlo con circa 100 richieste e si blocca al completamento della richiesta.
lopes710,

I second @ lopes710-- Questo sembra consentire a tutte le richieste di operare in parallelo, giusto?
Chris Prince,

se ho 2 richieste di rete, una nidificata con l'altra, all'interno di un ciclo for, quindi come assicurarsi che per ogni iterazione di ciclo for, entrambe le richieste siano state completate. ?
Awais Fayyaz,

@Channel, per favore c'è un modo per ottenere questo ordinato?
Israel Meshileya,

41

Swift 3 o 4

Se non ti interessano gli ordini , usa la risposta di @ paulvs , funziona perfettamente.

altrimenti, nel caso in cui qualcuno volesse ottenere il risultato in ordine anziché licenziarlo contemporaneamente, ecco il codice.

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

La mia app deve inviare più file a un server FTP, che include anche l'accesso per primo. Questo approccio garantisce che l'app acceda una sola volta (prima di caricare il primo file), invece di provare a farlo più volte, praticamente nello stesso momento (come con l'approccio "non ordinato"), il che provocherebbe errori. Grazie!
Neph,

Ho una domanda però: importa se lo fai dispatchSemaphore.signal()prima o dopo aver lasciato il dispatchGroup? Penseresti che sia meglio sbloccare il semaforo il più tardi possibile, ma non sono sicuro se e come lasciare il gruppo interferisca con quello. Ho testato entrambi gli ordini e non sembrava fare la differenza.
Neph,

16

Dettagli

  • Xcode 10.2.1 (10E1001), Swift 5

Soluzione

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

Campione completo

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

5

A tale scopo dovrai utilizzare i semafori.

 //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.

3

Swift 3: puoi anche usare i semafori in questo modo. Risulta molto utile, inoltre puoi tenere traccia esatta di quando e quali processi sono stati completati. Questo è stato estratto dal mio codice:

    //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...

1

Possiamo farlo con la ricorsione. Prendi idea dal codice seguente:

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

-1

Il gruppo di invio è buono ma l'ordine delle richieste inviate è casuale.

Finished request 1
Finished request 0
Finished request 2

Nel mio caso di progetto, ogni richiesta che doveva essere avviata è il giusto ordine. Se questo potesse aiutare qualcuno:

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

Chiama:

trySendRequestsNotSent()

Risultato:

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

Per ulteriori informazioni: Gist

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.