Aguarde até que o loop for rápido com solicitações de rede assíncronas termine a execução

159

Gostaria que um loop for in enviasse várias solicitações de rede para o firebase e passasse os dados para um novo controlador de exibição assim que o método concluir a execução. Aqui está o meu 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 

Eu tenho algumas preocupações. Primeiro, como espero até que o loop for seja concluído e todas as solicitações de rede sejam concluídas? Não consigo modificar a função observeSingleEventOfType, ela faz parte do SDK da base de firmas. Além disso, vou criar algum tipo de condição de corrida tentando acessar o dateArray de diferentes iterações do loop for (esperança que faça sentido)? Eu tenho lido sobre GCD e NSOperation, mas estou um pouco perdido, pois esse é o primeiro aplicativo que eu criei.

Nota: A matriz de locais é uma matriz que contém as chaves que preciso acessar no firebase. Além disso, é importante que as solicitações de rede sejam acionadas de forma assíncrona. Eu só quero esperar até que TODAS as solicitações assíncronas sejam concluídas antes de passar o dateArray para o próximo controlador de exibição.

Josh
fonte

Respostas:

338

Você pode usar grupos de expedição para disparar um retorno de chamada assíncrono quando todas as suas solicitações forem concluídas.

Aqui está um exemplo usando grupos de despacho para executar um retorno de chamada de forma assíncrona quando várias solicitações de rede foram concluídas.

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

Resultado

Finished request 1
Finished request 0
Finished request 2
Finished request 3
Finished request 4
Finished all requests.
paulvs
fonte
Isso funcionou muito bem! Obrigado! Você tem alguma idéia se eu vou encontrar alguma condição de corrida quando estou tentando atualizar o dateArray?
Josh
Não acho que exista uma condição de corrida aqui, porque todas as solicitações agregam valores ao datesArrayuso de uma chave diferente.
paulvs
1
@ Joshua Relativamente à condição de corrida: ocorre uma condição de corrida, se o mesmo local de memória for acessado a partir de threads diferentes, onde pelo menos um acesso é uma gravação - sem o uso de sincronização. Todos os acessos na mesma fila de expedição serial são sincronizados. A sincronização também ocorre com operações de memória que ocorrem na fila de despacho A, que é enviada para outra fila de despacho B. Todas as operações na fila A são sincronizadas na fila B. Portanto, se você olhar para a solução, não é garantido automaticamente que os acessos sejam sincronizados. ;)
CouchDeveloper
@josh, esteja ciente de que "programação de pista" é, em uma palavra, estupendamente difícil. Nunca é possível dizer instantaneamente "você tem / não tem um problema lá". Para programadores amadores: "simplesmente" sempre funcionam de uma maneira que significa que os problemas das pistas de corrida são, simplesmente, impossíveis. (Por exemplo, coisas como "só faça uma coisa de uma vez" etc.). Mesmo fazer isso é um grande desafio de programação.
Fattie
Muito legal. Mas eu tenho uma pergunta. Suponha que a solicitação 3 e a solicitação 4 falharam (por exemplo, erro do servidor, erro de autorização, qualquer coisa) e, em seguida, como chamar o loop novamente apenas para as solicitações restantes (solicitação 3 e solicitação 4)?
JD.
43

Xcode 8.3.1 - Swift 3

Esta é a resposta aceita de paulvs, convertida em 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
fonte
1
Oi, isso funciona para digamos 100 pedidos? ou 1000? Porque estou tentando fazer isso com cerca de 100 solicitações e está travando na conclusão da solicitação.
lopes710
Eu segundo @ lopes710-- Isso parece permitir que todos os pedidos operem em paralelo, certo?
Chris Príncipe
se eu tiver 2 solicitações de rede, uma aninhada com a outra, dentro de um loop for, como garantir que, para cada iteração do loop for, ambas as solicitações sejam concluídas. ?
Awais Fayyaz 31/08/19
@ Canal, por favor, existe uma maneira de obter isso ordenado?
Israel Meshileya 5/05/19
41

Swift 3 ou 4

Se você não se importa com pedidos , use a resposta da @ paulvs , ela funciona perfeitamente.

caso contrário, se alguém quiser obter o resultado em ordem, em vez de acioná-lo simultaneamente, aqui está o 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
fonte
Meu aplicativo precisa enviar vários arquivos para um servidor FTP, o que também inclui o login primeiro. Essa abordagem garante que o aplicativo efetue login apenas uma vez (antes de carregar o primeiro arquivo), em vez de tentar fazê-lo várias vezes, basicamente ao mesmo tempo (como na abordagem "não ordenada"), o que provocaria erros. Obrigado!
Neph 5/12
Mas eu tenho uma pergunta: importa se você faz isso dispatchSemaphore.signal()antes ou depois de sair do dispatchGroup? Você acha que é melhor desbloquear o semáforo o mais tarde possível, mas não tenho certeza se e como a saída do grupo interfere nisso. Testei os dois pedidos e não pareceu fazer diferença.
Neph 5/12
16

Detalhes

  • Xcode 10.2.1 (10E1001), Swift 5

Solução

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

Amostra 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
fonte
5

Você precisará usar semáforos para esse fim.

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

Swift 3: Você também pode usar semáforos dessa maneira. Isso resulta muito útil, além de você poder acompanhar exatamente quando e quais processos foram concluídos. Isso foi extraído do meu 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
fonte
1

Podemos fazer isso com recursão. Obtenha uma ideia do código abaixo:

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()
        }
    }
}
Deep
fonte
-1

O grupo de distribuição é bom, mas a ordem dos pedidos enviados é aleatória.

Finished request 1
Finished request 0
Finished request 2

No meu caso de projeto, cada solicitação necessária para o lançamento é a ordem certa. Se isso puder ajudar alguém:

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

Ligar :

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

Veja mais informações: Gist

Aximem
fonte