Tunggu hingga swift for loop dengan permintaan jaringan asinkron selesai dieksekusi

159

Saya ingin agar dalam loop untuk mengirim sekelompok permintaan jaringan ke firebase, kemudian meneruskan data ke pengontrol tampilan baru setelah metode selesai dijalankan. Ini kode saya:

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 

Saya punya beberapa masalah. Pertama, bagaimana saya menunggu sampai loop selesai dan semua permintaan jaringan selesai? Saya tidak dapat memodifikasi fungsi observSingleEventOfType, ini adalah bagian dari SDK firebase. Juga, akankah saya membuat semacam kondisi balapan dengan mencoba mengakses DateArray dari berbagai iterations for for (harapan yang masuk akal)? Saya sudah membaca tentang GCD dan NSOperation tapi saya agak bingung karena ini adalah aplikasi pertama yang saya buat.

Catatan: Array lokasi adalah array yang berisi kunci yang perlu saya akses di firebase. Juga, penting bahwa permintaan jaringan dimatikan secara tidak sinkron. Saya hanya ingin menunggu sampai SEMUA permintaan asinkron selesai sebelum saya melewatkan DateArray ke pengontrol tampilan berikutnya.

Josh
sumber

Jawaban:

338

Anda dapat menggunakan grup pengiriman untuk memecat panggilan balik yang tidak sinkron ketika semua permintaan Anda selesai.

Berikut adalah contoh menggunakan grup pengiriman untuk melakukan panggilan balik secara tidak sinkron ketika beberapa permintaan jaringan telah selesai.

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

Keluaran

Finished request 1
Finished request 0
Finished request 2
Finished request 3
Finished request 4
Finished all requests.
paulvs
sumber
Ini bekerja dengan baik! Terima kasih! Apakah Anda tahu jika saya akan mengalami kondisi balapan ketika saya mencoba memperbarui DateArray?
Josh
Saya tidak berpikir ada kondisi balapan di sini karena semua permintaan menambah nilai datesArraymenggunakan kunci yang berbeda.
paulvs
1
@Josh Mengenai kondisi balapan: terjadi kondisi balapan, jika lokasi memori yang sama akan diakses dari utas yang berbeda, di mana setidaknya satu akses adalah tulis - tanpa menggunakan sinkronisasi. Semua akses dalam antrian pengiriman serial yang sama disinkronkan. Sinkronisasi juga terjadi dengan operasi memori yang terjadi pada antrian pengiriman A, yang mengirimkan ke antrian pengiriman lain B. Semua operasi dalam antrian A kemudian disinkronkan dalam antrian B. Jadi, jika Anda melihat solusinya, tidak secara otomatis dijamin bahwa akses disinkronkan. ;)
CouchDeveloper
@ josh, ketahuilah bahwa "pemrograman pacuan kuda" dalam kata lain sangat sulit. Tidak pernah mungkin untuk langsung mengatakan "Anda memang tidak punya masalah di sana." Untuk programmer hobi: "sederhananya" selalu bekerja dengan cara yang berarti masalah pacuan kuda, sederhana, tidak mungkin. (Misalnya, hal-hal seperti "hanya lakukan satu hal sekaligus" dll.) Melakukan hal itu adalah tantangan pemrograman yang sangat besar.
Fattie
Sangat keren. Tapi saya punya pertanyaan. Misalkan permintaan 3 dan permintaan 4 gagal (mis. Kesalahan server, kesalahan otorisasi, apa pun), lalu bagaimana cara menelepon untuk loop lagi hanya untuk permintaan yang tersisa (permintaan 3 & permintaan 4)?
JD.
43

Xcode 8.3.1 - Swift 3

Ini adalah jawaban yang diterima dari paulvs, dikonversi ke 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.")
    })
}
Saluran
sumber
1
Hai, apakah ini berfungsi untuk katakanlah 100 permintaan? atau 1000? Karena saya mencoba melakukan ini dengan sekitar 100 permintaan dan terhenti saat penyelesaian permintaan.
lopes710
I second @ lopes710-- Tampaknya ini memungkinkan semua permintaan untuk beroperasi secara paralel, bukan?
Chris Prince
jika saya memiliki 2 permintaan jaringan, satu bersarang dengan yang lain, di dalam for loop, lalu bagaimana memastikan bahwa untuk setiap iterasi for loop, kedua permintaan telah selesai. ?
Awais Fayyaz
@ Channel, tolong apakah ada cara saya bisa memesan ini?
Israel Meshileya
41

Swift 3 atau 4

Jika Anda tidak peduli dengan pesanan , gunakan jawaban @ paulvs , itu berfungsi dengan baik.

kalau-kalau ada orang yang ingin mendapatkan hasilnya agar bukannya memecat mereka secara bersamaan, berikut adalah kodenya.

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()
        }
    }
}
Abadi
sumber
Aplikasi saya harus mengirim banyak file ke server FTP, yang juga termasuk masuk terlebih dahulu. Pendekatan ini menjamin bahwa aplikasi hanya login sekali (sebelum mengunggah file pertama), alih-alih mencoba melakukannya berkali-kali, semuanya pada dasarnya bersamaan (seperti dengan pendekatan "tidak berurutan"), yang akan memicu kesalahan. Terima kasih!
Neph
Saya punya satu pertanyaan: Apakah penting jika Anda melakukannya dispatchSemaphore.signal()sebelum atau setelah meninggalkan dispatchGroup? Anda akan berpikir bahwa yang terbaik untuk membuka blokir semafor selambat mungkin, tetapi saya tidak yakin apakah dan bagaimana meninggalkan grup mengganggu itu. Saya menguji kedua pesanan dan sepertinya tidak ada bedanya.
Neph
16

Detail

  • Xcode 10.2.1 (10E1001), Swift 5

Larutan

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

Pemakaian

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

Sampel lengkap

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() }
    }
}
Bodnarchuk dengan mudah
sumber
5

Anda perlu menggunakan semaphores untuk tujuan ini.

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

Swift 3: Anda juga bisa menggunakan semaphores dengan cara ini. Hasilnya sangat membantu, selain itu Anda dapat melacak kapan dan proses apa yang selesai. Ini telah diekstraksi dari kode saya:

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

Kita bisa melakukan ini dengan rekursi. Dapatkan ide dari kode di bawah ini:

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()
        }
    }
}
Dalam
sumber
-1

Grup pengiriman baik tetapi urutan permintaan yang dikirim acak.

Finished request 1
Finished request 0
Finished request 2

Dalam kasus proyek saya, setiap permintaan yang perlu diluncurkan adalah urutan yang benar. Jika ini bisa membantu seseorang:

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

Hubungi:

trySendRequestsNotSent()

Hasil:

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

Lihat info lebih lanjut: Inti

Aximem
sumber