Attendez que la boucle rapide pour avec les requêtes réseau asynchrones se termine

159

Je voudrais qu'une boucle for in envoie un tas de requêtes réseau à Firebase, puis transmette les données à un nouveau contrôleur de vue une fois que la méthode a terminé son exécution. Voici mon code:

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 

J'ai quelques inquiétudes. Tout d'abord, comment attendre que la boucle for soit terminée et que toutes les demandes réseau soient terminées? Je ne peux pas modifier la fonction observeSingleEventOfType, elle fait partie du SDK Firebase. De plus, vais-je créer une sorte de condition de concurrence en essayant d'accéder aux datesArray à partir de différentes itérations de la boucle for (j'espère que cela a du sens)? J'ai lu sur GCD et NSOperation mais je suis un peu perdu car c'est la première application que j'ai créée.

Remarque: le tableau Emplacements est un tableau contenant les clés auxquelles j'ai besoin d'accéder dans Firebase. En outre, il est important que les requêtes réseau soient déclenchées de manière asynchrone. Je veux juste attendre que TOUTES les demandes asynchrones soient terminées avant de transmettre le dateArray au contrôleur de vue suivant.

Josh
la source

Réponses:

338

Vous pouvez utiliser des groupes de répartition pour déclencher un rappel asynchrone lorsque toutes vos demandes sont terminées.

Voici un exemple d'utilisation de groupes de répartition pour exécuter un rappel de manière asynchrone lorsque plusieurs demandes de mise en réseau sont toutes terminées.

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

Production

Finished request 1
Finished request 0
Finished request 2
Finished request 3
Finished request 4
Finished all requests.
Paulvs
la source
Cela a très bien fonctionné! Merci! Avez-vous une idée si je vais rencontrer des conditions de course lorsque j'essaye de mettre à jour le datesArray?
Josh
Je ne pense pas qu'il y ait une condition de concurrence ici car toutes les demandes ajoutent des valeurs à l' datesArrayutilisation d'une clé différente.
paulvs
1
@Josh Concernant la condition de concurrence: une condition de concurrence se produit, si le même emplacement de mémoire sera accessible à partir de différents threads, où au moins un accès est une écriture - sans utiliser la synchronisation. Cependant, tous les accès au sein de la même file d'attente de distribution série sont synchronisés. La synchronisation se produit également avec les opérations de mémoire se produisant sur la file d'attente de distribution A, qui se soumet à une autre file d'attente de distribution B.Toutes les opérations de la file d'attente A sont ensuite synchronisées dans la file d'attente B. Ainsi, si vous examinez la solution, il n'est pas automatiquement garanti que les accès sont synchronisés. ;)
CouchDeveloper
@josh, sachez que la "programmation hippodrome" est, en un mot, incroyablement difficile. Il n'est jamais possible de dire instantanément «vous avez / n'avez pas de problème». Pour les programmeurs amateurs: "simplement" travaillez toujours de manière à ce que les problèmes de piste soient, tout simplement, impossibles. (Par exemple, des choses comme "ne faire qu'une seule chose à la fois", etc.) Même faire cela est un énorme défi de programmation.
Fattie
Super cool. Mais j'ai une question. Supposons que la requête 3 et la requête 4 aient échoué (par exemple, erreur de serveur, erreur d'autorisation, quoi que ce soit), alors comment appeler à nouveau la boucle pour les requêtes restantes uniquement (requête 3 et requête 4)?
JD.
43

Xcode 8.3.1 - Swift 3

C'est la réponse acceptée de paulvs, convertie en 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
la source
1
Salut, cela fonctionne-t-il pour 100 demandes? ou 1000? Parce que j'essaie de faire cela avec environ 100 demandes et que je plante à la fin de la demande.
lopes710
Je seconde @ lopes710 - Cela semble permettre à toutes les demandes de fonctionner en parallèle, non?
Chris Prince
si j'ai 2 demandes réseau, l'une imbriquée dans l'autre, dans une boucle for, alors comment m'assurer que pour chaque itération de la boucle for, les deux demandes ont été complétées. ?
Awais Fayyaz
@Channel, y a-t-il un moyen de le commander?
Israel Meshileya
41

Swift 3 ou 4

Si vous ne vous souciez pas des commandes , utilisez la réponse de @ paulvs , cela fonctionne parfaitement.

sinon, juste au cas où quelqu'un voudrait obtenir le résultat dans l'ordre au lieu de les déclencher simultanément, voici le code.

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()
        }
    }
}
Intemporel
la source
Mon application doit envoyer plusieurs fichiers à un serveur FTP, ce qui inclut également la connexion en premier. Cette approche garantit que l'application ne se connecte qu'une seule fois (avant de télécharger le premier fichier), au lieu d'essayer de le faire plusieurs fois, le tout fondamentalement en même temps (comme avec l'approche «non ordonnée»), ce qui déclencherait des erreurs. Merci!
Neph
J'ai une question cependant: est-ce important que vous le fassiez dispatchSemaphore.signal()avant ou après avoir quitté le dispatchGroup? Vous penseriez qu'il est préférable de débloquer le sémaphore le plus tard possible, mais je ne sais pas si et comment quitter le groupe interfère avec cela. J'ai testé les deux commandes et cela n'a pas semblé faire de différence.
Neph
16

Détails

  • Xcode 10.2.1 (10E1001), Swift 5

Solution

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

Usage

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

Échantillon complet

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
la source
5

Vous devrez utiliser des sémaphores à cette fin.

 //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
la source
3

Swift 3: Vous pouvez également utiliser des sémaphores de cette manière. Cela s'avère très utile, en plus, vous pouvez suivre exactement quand et quels processus sont terminés. Ceci a été extrait de mon code:

    //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
la source
1

Nous pouvons le faire avec la récursivité. Obtenez une idée du code ci-dessous:

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()
        }
    }
}
Profond
la source
-1

Le groupe d'expédition est bon mais l'ordre des demandes envoyées est aléatoire.

Finished request 1
Finished request 0
Finished request 2

Dans mon cas de projet, chaque demande nécessaire pour être lancée est la bonne commande. Si cela pouvait aider quelqu'un:

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

Appel :

trySendRequestsNotSent()

Résultat :

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

Voir pour plus d'infos: Gist

Aximem
la source