Poczekaj, aż funkcja Swift for loop z asynchronicznymi żądaniami sieciowymi zakończy wykonywanie

159

Chciałbym, aby pętla for in wysyłała kilka żądań sieciowych do firebase, a następnie przekazywała dane do nowego kontrolera widoku po zakończeniu wykonywania metody. Oto mój kod:

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 

Mam kilka obaw. Po pierwsze, jak mam czekać, aż pętla for zostanie zakończona i wszystkie żądania sieciowe zostaną zakończone? Nie mogę zmodyfikować funkcji observSingleEventOfType, jest ona częścią zestawu SDK Firebase. Ponadto, czy utworzę jakiś rodzaj wyścigu, próbując uzyskać dostęp do tablicy dat z różnych iteracji pętli for (mam nadzieję, że ma to sens)? Czytałem o GCD i NSOperation, ale jestem trochę zagubiony, ponieważ jest to pierwsza aplikacja, którą zbudowałem.

Uwaga: Tablica lokalizacji to tablica zawierająca klucze, do których potrzebuję dostępu w firebase. Ponadto ważne jest, aby żądania sieciowe były odpalane asynchronicznie. Chcę tylko poczekać, aż WSZYSTKIE asynchroniczne żądania zakończą się, zanim przekażę dateArray do następnego kontrolera widoku.

Josh
źródło

Odpowiedzi:

338

Możesz użyć grup wysyłkowych aby uruchomić asynchroniczne wywołanie zwrotne po zakończeniu wszystkich żądań.

Oto przykład użycia grup dyspozytorskich do asynchronicznego wykonywania wywołania zwrotnego po zakończeniu wielu żądań sieciowych.

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

Wynik

Finished request 1
Finished request 0
Finished request 2
Finished request 3
Finished request 4
Finished all requests.
Paulvs
źródło
To działało świetnie! Dzięki! Czy masz pojęcie, czy napotkam jakieś warunki wyścigu, kiedy próbuję zaktualizować tablicę dat?
Josh
Myślę, że nie ma tu warunków wyścigu, ponieważ wszystkie żądania dodają wartości do datesArrayużycia innego klucza.
paulvs
1
@Josh Odnośnie sytuacji wyścigu: sytuacja wyścigu występuje, jeśli ta sama lokalizacja pamięci będzie dostępna z różnych wątków, gdzie co najmniej jeden dostęp to zapis - bez użycia synchronizacji. Wszystkie dostępy w ramach tej samej kolejki wysyłania szeregowego są jednak synchronizowane. Synchronizacja występuje również w przypadku operacji pamięciowych występujących w kolejce wysyłkowej A, która przesyła się do innej kolejki wysyłkowej B. Wszystkie operacje w kolejce A są następnie synchronizowane w kolejce B. Jeśli więc spojrzysz na rozwiązanie, nie ma automatycznej gwarancji, że dostępy są zsynchronizowane. ;)
CouchDeveloper
@josh, pamiętaj, że „programowanie torów wyścigowych” jest, jednym słowem, zdumiewająco trudne. Nigdy nie jest możliwe natychmiastowe powiedzenie „masz / nie masz tam problemu”. Dla programistów hobbystów: „po prostu” zawsze działa w sposób, który oznacza, że ​​problemy z torem wyścigowym są po prostu niemożliwe. (Na przykład takie rzeczy jak „rób tylko jedną rzecz naraz” itp.) Nawet zrobienie tego jest ogromnym wyzwaniem programistycznym.
Fattie
Super fajne. Ale mam pytanie. Załóżmy, że żądanie 3 i żądanie 4 nie powiodły się (np. Błąd serwera, błąd autoryzacji, cokolwiek), a następnie jak ponownie wywołać pętlę tylko dla pozostałych żądań (żądanie 3 i żądanie 4)?
JD.
43

Xcode 8.3.1 - Swift 3

Oto akceptowana odpowiedź Paulvów, przekonwertowana na 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.")
    })
}
Kanał
źródło
1
Cześć, czy to działa dla powiedzmy 100 żądań? czy 1000? Ponieważ próbuję to zrobić z około 100 żądaniami i ulega awarii po zakończeniu żądania.
lopes710
I second @ lopes710 - Wygląda na to, że wszystkie żądania działają równolegle, prawda?
Chris Prince,
jeśli mam 2 żądania sieciowe, jedno zagnieżdżone w drugim, wewnątrz pętli for, to jak się upewnić, że dla każdej iteracji pętli for oba żądania zostały zakończone. ?
Awais Fayyaz
@Channel, czy jest sposób, w jaki mogę to zamówić?
Israel Meshileya
41

Swift 3 lub 4

Jeśli nie dbają o zamówieniach , korzystanie @ paulvs męska odpowiedź , to działa doskonale.

inaczej na wszelki wypadek, gdyby ktoś chciał uzyskać wynik w kolejności zamiast odpalać je jednocześnie, oto kod.

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()
        }
    }
}
Ponadczasowy
źródło
Moja aplikacja musi wysłać wiele plików na serwer FTP, co obejmuje również wcześniejsze zalogowanie się. Takie podejście gwarantuje, że aplikacja loguje się tylko raz (przed przesłaniem pierwszego pliku), zamiast próbować to zrobić wiele razy, w zasadzie wszystko w tym samym czasie (jak w przypadku podejścia „nieuporządkowanego”), co spowodowałoby błędy. Dzięki!
Neph
Mam jednak jedno pytanie: czy to ważne, czy zrobisz to dispatchSemaphore.signal()przed, czy po opuszczeniu dispatchGroup? Można by pomyśleć, że najlepiej odblokować semafor tak późno, jak to możliwe, ale nie jestem pewien, czy i jak opuszczenie grupy to przeszkadza. Przetestowałem oba zamówienia i nie miało to znaczenia.
Neph
16

Detale

  • Xcode 10.2.1 (10E1001), Swift 5

Rozwiązanie

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

Stosowanie

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

Pełna próbka

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() }
    }
}
Wasilij Bodnarchuk
źródło
5

W tym celu będziesz musiał użyć semaforów.

 //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
źródło
3

Swift 3: W ten sposób możesz również użyć semaforów. Jest to bardzo pomocne, poza tym możesz dokładnie śledzić, kiedy i jakie procesy są zakończone. Zostało to wyodrębnione z mojego kodu:

    //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
źródło
1

Możemy to zrobić z rekurencją. Uzyskaj pomysł z poniższego kodu:

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()
        }
    }
}
Głęboki
źródło
-1

Grupa wysyłkowa jest dobra, ale kolejność wysyłanych zapytań jest losowa.

Finished request 1
Finished request 0
Finished request 2

W przypadku mojego projektu każda prośba o uruchomienie to właściwa kolejność. Jeśli to mogłoby komuś pomóc:

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

Połączenie :

trySendRequestsNotSent()

Wynik:

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

Zobacz więcej informacji: Gist

Aximem
źródło