Зачекайте, поки завершення виконання циклу з асинхронними мережевими запитами завершиться


159

Я хотів би, щоб цикл for 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 завершиться і всі запити мережі будуть завершені? Я не можу змінити функцію obserSingleEventOfType, вона є частиною SDK firebase. Крім того, чи створять я якусь умову гонки, намагаючись отримати доступ до dateArray з різних ітерацій циклу for (сподіваюся, що це має сенс)? Я читав про GCD та NSOperation, але трохи розгубився, оскільки це перший додаток, який я створив.

Примітка. Масив Locations - це масив, що містить ключі, до яких мені потрібно отримати доступ до firebase. Крім того, важливо, щоб запити мережі були відключені асинхронно. Я просто хочу зачекати, поки ВСІ асинхронні запити завершаться, перш ніж передати датуАrray на наступний контролер перегляду.

Відповіді:


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.

Це спрацювало чудово! Дякую! Чи маєте ви ідею, чи зіткнуся я з будь-якими умовами гонки, коли я намагаюся оновити датиArray?
Джош

Я не думаю, що тут є умова перегонів, оскільки всі запити додають значення для datesArrayвикористання іншого ключа.
paulvs

1
@Josh Щодо умови перегонів: стан гонки виникає, якщо до одного і того ж місця пам'яті буде доступ з різних потоків, де принаймні один доступ є записом - без використання синхронізації. Хоча всі звернення в одній черзі послідовних відправлень синхронізовані. Синхронізація також відбувається з операціями пам'яті, що відбуваються в черзі відправлення A, яка передає іншу диспетчерську чергу B. Всі операції в черзі A синхронізуються в черзі B. Отже, якщо ви подивитесь на рішення, не гарантується автоматично, що доступ синхронізований. ;)
CouchDeveloper

@josh, пам’ятай, що «програмування іподрому», одним словом, надзвичайно складно. Ніколи не можна просто миттєво сказати "у вас це / не маєте проблеми". Для програмістів-любителів: "просто" завжди працювати таким чином, що означає проблеми з іподромом, просто неможливо. (Наприклад, такі речі, як "робити лише одну справу одночасно" тощо). Навіть це - величезна проблема програмування.
Fattie

Супер круто. Але у мене є питання. Припустимо, що запит 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

Я другий @ lopes710-- Здається, це дозволяє всім запитам працювати паралельно, правда?
Кріс Принс

якщо у мене є 2 мережеві запити, один вкладений з іншим, всередині циклу a, то як переконатися, що для кожної ітерації циклу обидва запити були виконані. ?
Awais Fayyaz

@Channel, будь ласка, чи можна замовити це?
Ізраїль Мешилея

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.

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.