Warten Sie, bis die Ausführung der schnellen for-Schleife mit asynchronen Netzwerkanforderungen abgeschlossen ist


159

Ich möchte, dass eine for-in-Schleife eine Reihe von Netzwerkanforderungen an firebase sendet und die Daten dann an einen neuen View-Controller weiterleitet, sobald die Ausführung der Methode abgeschlossen ist. Hier ist mein 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 

Ich habe ein paar Bedenken. Wie warte ich zunächst, bis die for-Schleife abgeschlossen ist und alle Netzwerkanforderungen abgeschlossen sind? Ich kann die Funktion compareSingleEventOfType nicht ändern, sie ist Teil des Firebase-SDK. Werde ich auch eine Art Race-Bedingung erstellen, indem ich versuche, aus verschiedenen Iterationen der for-Schleife auf das Datumsarray zuzugreifen (hoffe, das macht Sinn)? Ich habe über GCD und NSOperation gelesen, bin aber etwas verloren, da dies die erste App ist, die ich erstellt habe.

Hinweis: Das Locations-Array ist ein Array, das die Schlüssel enthält, auf die ich in Firebase zugreifen muss. Außerdem ist es wichtig, dass die Netzwerkanforderungen asynchron ausgelöst werden. Ich möchte nur warten, bis ALLE asynchronen Anforderungen abgeschlossen sind, bevor ich das Datumsarray an den nächsten Ansichtscontroller übergebe.

Antworten:


338

Sie können Versandgruppen verwenden , um einen asynchronen Rückruf auszulösen, wenn alle Ihre Anforderungen abgeschlossen sind.

Hier ist ein Beispiel für die Verwendung von Versandgruppen, um einen Rückruf asynchron auszuführen, wenn mehrere Netzwerkanforderungen abgeschlossen sind.

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

Ausgabe

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

Das hat super geklappt! Vielen Dank! Haben Sie eine Idee, ob ich beim Versuch, die Daten zu aktualisieren, auf Rennbedingungen stoßen werde?
Josh

Ich glaube nicht, dass es hier eine Race-Bedingung gibt, da alle Anfragen der datesArrayVerwendung eines anderen Schlüssels Werte hinzufügen .
Paulvs

1
@Josh Bezüglich der Racebedingung: Eine Racebedingung tritt auf, wenn von verschiedenen Threads auf denselben Speicherort zugegriffen wird, wobei mindestens ein Zugriff ein Schreibzugriff ist - ohne Verwendung der Synchronisation. Alle Zugriffe innerhalb derselben seriellen Versandwarteschlange werden jedoch synchronisiert. Die Synchronisierung erfolgt auch mit Speichervorgängen in der Versandwarteschlange A, die an eine andere Versandwarteschlange B gesendet wird. Alle Vorgänge in Warteschlange A werden dann in Warteschlange B synchronisiert. Wenn Sie sich also die Lösung ansehen, wird nicht automatisch garantiert, dass die Zugriffe synchronisiert werden. ;)
CouchDeveloper

@josh, sei dir bewusst, dass "Rennstreckenprogrammierung" mit einem Wort erstaunlich schwierig ist. Es ist nie möglich, sofort zu sagen: "Sie haben dort kein Problem." Für Hobby-Programmierer: "Einfach" immer so arbeiten, dass Rennstreckenprobleme einfach unmöglich sind. (Zum Beispiel Dinge wie "nur eine Sache gleichzeitig machen" usw.) Selbst das zu tun ist eine große Herausforderung für die Programmierung.
Fattie

Super cool. Aber ich habe eine Frage. Angenommen, Anforderung 3 und Anforderung 4 sind fehlgeschlagen (z. B. Serverfehler, Autorisierungsfehler usw.). Wie kann dann die for-Schleife nur für verbleibende Anforderungen erneut aufgerufen werden (Anforderung 3 und Anforderung 4)?
JD.

43

Xcode 8.3.1 - Swift 3

Dies ist die akzeptierte Antwort von Paulvs, konvertiert zu 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
Hallo, funktioniert das für sagen wir 100 Anfragen? oder 1000? Weil ich dies mit ungefähr 100 Anfragen versuche und nach Abschluss der Anfrage abstürzt.
Lopes710

I second @ lopes710-- Dies scheint zu ermöglichen, dass alle Anforderungen parallel ausgeführt werden, oder?
Chris Prince

Wenn ich zwei Netzwerkanforderungen habe, eine mit der anderen in einer for-Schleife verschachtelt, wie kann sichergestellt werden, dass für jede Iteration der for-Schleife beide Anforderungen abgeschlossen wurden? ?
Awais Fayyaz

@Channel, bitte gibt es eine Möglichkeit, wie ich das bestellen kann?
Israel Meshileya

41

Swift 3 oder 4

Wenn Sie sich nicht für Bestellungen interessieren, verwenden Sie die Antwort von @ paulvs , es funktioniert perfekt.

sonst nur für den Fall, dass jemand das Ergebnis in der richtigen Reihenfolge erhalten möchte, anstatt es gleichzeitig abzufeuern, hier ist der 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()
        }
    }
}

Meine App muss mehrere Dateien an einen FTP-Server senden. Dazu gehört auch, dass Sie sich zuerst anmelden. Dieser Ansatz garantiert, dass sich die App nur einmal anmeldet (vor dem Hochladen der ersten Datei), anstatt mehrmals zu versuchen, dies im Grunde zur gleichen Zeit (wie beim "ungeordneten" Ansatz), was zu Fehlern führen würde. Vielen Dank!
Neph

Ich habe jedoch eine Frage: Ist es wichtig, ob Sie dies dispatchSemaphore.signal()vor oder nach dem Verlassen der tun dispatchGroup? Sie würden denken, dass es am besten ist, das Semaphor so spät wie möglich zu entsperren, aber ich bin mir nicht sicher, ob und wie das Verlassen der Gruppe dies stört. Ich habe beide Bestellungen getestet und es schien keinen Unterschied zu machen.
Neph

16

Einzelheiten

  • Xcode 10.2.1 (10E1001), Swift 5

Lösung

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

Verwendung

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

Vollständige Probe

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

Zu diesem Zweck müssen Sie Semaphoren verwenden.

 //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: Auf diese Weise können Sie auch Semaphoren verwenden. Es ist sehr hilfreich, außerdem können Sie genau verfolgen, wann und welche Prozesse abgeschlossen sind. Dies wurde aus meinem Code extrahiert:

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

Wir können dies mit Rekursion tun. Machen Sie sich ein Bild von dem folgenden Code:

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

Die Versandgruppe ist gut, aber die Reihenfolge der gesendeten Anfragen ist zufällig.

Finished request 1
Finished request 0
Finished request 2

In meinem Projektfall ist jede Anforderung, die gestartet werden muss, die richtige Reihenfolge. Wenn dies jemandem helfen könnte:

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

Anruf :

trySendRequestsNotSent()

Ergebnis:

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

Weitere Informationen finden Sie unter: Gist

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.