Дождитесь окончания выполнения цикла swift for loop с асинхронными сетевыми запросами

159

Я хотел бы, чтобы цикл in отправлял кучу сетевых запросов в firebase, а затем передавал данные новому контроллеру представления после завершения метода. Вот мой код:

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 

У меня есть пара проблем. Во-первых, как мне дождаться завершения цикла for и завершения всех сетевых запросов? Я не могу изменить функцию наблюдаем единыйEventOfType, это часть SDK Firebase. Кроме того, я создам какое-то условие гонки, пытаясь получить доступ к dateArray из разных итераций цикла for (надеюсь, это имеет смысл)? Я читал о GCD и NSOperation, но я немного растерялся, так как это первое приложение, которое я создал.

Примечание: массив Locations - это массив, содержащий ключи, которые мне нужны для доступа в firebase. Также важно, чтобы сетевые запросы запускались асинхронно. Я просто хочу подождать, пока ВСЕ асинхронные запросы завершатся, прежде чем я передам dateArray следующему контроллеру представления.

мистифицировать
источник

Ответы:

338

Вы можете использовать группы рассылки для запуска асинхронного обратного вызова, когда все ваши запросы завершатся.

Вот пример использования групп диспетчеризации для асинхронного выполнения обратного вызова, когда все сетевые запросы завершены.

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

Вывод

Finished request 1
Finished request 0
Finished request 2
Finished request 3
Finished request 4
Finished all requests.
paulvs
источник
Это сработало отлично! Спасибо! Есть ли у вас идеи, столкнусь ли я с какими-либо условиями гонки, когда попытаюсь обновить dateArray?
Джош
Я не думаю, что здесь есть условие гонки, потому что все запросы добавляют значения к datesArrayиспользованию другого ключа.
paulvs
1
@Josh Относительно состояния гонки: условие гонки возникает, если к одной и той же ячейке памяти будут обращаться из разных потоков, где хотя бы один доступ - запись - без использования синхронизации. Однако все обращения в одной и той же очереди последовательной отправки синхронизируются. Синхронизация также происходит с операциями с памятью, происходящими в очереди отправки A, которая передает в другую очередь отправки B. Все операции в очереди A затем синхронизируются в очереди B. Таким образом, если вы посмотрите на решение, автоматически не гарантируется, что доступы синхронизированы. ;)
CouchDeveloper
@josh, помни, что «программирование ипподрома», одним словом, невероятно сложно. Никогда нельзя просто сразу сказать «у вас есть / нет проблем там». Для программистов-любителей: «просто» всегда работает так, что это означает, что проблемы с гоночной трассой просто невозможны. (Например, такие вещи, как «делать только одну вещь одновременно» и т. Д.). Даже делать это - огромная задача для программирования.
Толстяк
Очень круто. Но у меня есть вопрос. Предположим, что запрос 3 и запрос 4 завершились неудачно (например, ошибка сервера, ошибка авторизации и т. Д.), Тогда как снова вызвать цикл только для оставшихся запросов (запрос 3 и запрос 4)?
JD.
43

Xcode 8.3.1 - Swift 3

Это принятый ответ Паульва, преобразованный в 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
Привет, это работает, скажем, на 100 запросов? или 1000? Потому что я пытаюсь сделать это с около 100 запросов и сбой при завершении запроса.
lopes710
I second @ lopes710-- Похоже, что все запросы могут работать параллельно, верно?
Крис Принс
если у меня есть 2 сетевых запроса, один вложенный с другим, внутри цикла for, то как убедиться, что для каждой итерации цикла оба запроса были выполнены. ?
Awais Fayyaz
@ Канал, пожалуйста, можно ли как-нибудь заказать это?
Израиль Мешилея
41

Свифт 3 или 4

Если вас не интересуют заказы , используйте ответ @ paulvs , он работает отлично.

В противном случае, если кто-то хочет получить результат по порядку, а не запускать их одновременно, вот код.

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()
        }
    }
}
вневременный
источник
Мое приложение должно отправить несколько файлов на FTP-сервер, что также включает в себя сначала вход в систему. Такой подход гарантирует, что приложение будет входить в систему только один раз (перед загрузкой первого файла), вместо того, чтобы пытаться сделать это несколько раз, в основном в одно и то же время (как в случае «неупорядоченного» подхода), что приведет к ошибкам. Спасибо!
Неф
У меня есть один вопрос: имеет ли значение, если вы делаете dispatchSemaphore.signal()до или после ухода dispatchGroup? Вы могли бы подумать, что лучше разблокировать семафор как можно позже, но я не уверен, мешает ли это покинуть группу. Я проверил оба заказа, и это, похоже, не имело значения.
неф
16

подробности

  • Xcode 10.2.1 (10E1001), Swift 5

Решение

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

использование

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

Полный образец

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

Вам нужно будет использовать семафоры для этой цели.

 //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
источник
3

Swift 3: Вы также можете использовать семафоры на этом пути. Это очень полезно, кроме того, вы можете точно отслеживать, когда и какие процессы завершены. Это было извлечено из моего кода:

    //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
источник
1

Мы можем сделать это с помощью рекурсии. Получить идею из кода ниже:

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

Группа рассылки хороша, но порядок отправленных запросов случайный.

Finished request 1
Finished request 0
Finished request 2

В моем случае проекта, каждый запрос должен быть запущен, это правильный порядок. Если бы это могло помочь кому-то:

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

Вызов :

trySendRequestsNotSent()

Результат:

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

Смотрите больше информации: Gist

Aximem
источник