非同期ネットワークリクエストを含むSwift forループの実行が完了するまで待機します


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ループが終了してすべてのネットワーク要求が完了するまで、どのように待つのですか?observeSingleEventOfType関数は変更できません。FirebaseSDKの一部です。また、forループのさまざまな反復からdatesArrayにアクセスしようとすることで、ある種の競合状態を作成しますか(意味があると思います)。私はGCDとNSOperationについて読んでいましたが、これは私が作成した最初のアプリであるため、少し迷っています。

注:Locations配列は、firebaseでアクセスする必要があるキーを含む配列です。また、ネットワーク要求が非同期で発生することも重要です。すべての非同期リクエストが完了するまで待ってから、datesArrayを次のビューコントローラーに渡します。

回答:


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.

これはうまくいきました!ありがとう!DatesArrayを更新しようとしているときに、競合状態に遭遇するかどうか考えていますか?
Josh

すべてのリクエストdatesArrayが別のキーを使用して値を追加するため、ここでは競合状態はないと思います。
paulvs 2016年

1
@Josh競合状態に関して:同じメモリ位置が異なるスレッドからアクセスされ、少なくとも1つのアクセスが書き込みである場合に、同期使用せずに競合状態が発生します。ただし、同じシリアルディスパッチキュー内のすべてのアクセスは同期されます。同期は、ディスパッチキューAで発生するメモリ操作でも発生します。ディスパッチキューAは、別のディスパッチキューBにサブミットします。キューAのすべての操作は、キューBで同期されます。したがって、ソリューションを確認しても、アクセスの同期は自動的には保証されません。;)
CouchDeveloper 2016年

@josh、一言で言えば、「競馬場プログラミング」は途方もなく困難であることに注意してください。「あなたはそこに問題を抱えている/抱えていない」と即座に言うことは決して不可能です。趣味のプログラマーの場合:「単純に」は常に、競馬場の問題が単純に不可能であることを意味する方法で機能します。(たとえば、「一度に1つのことだけを行う」など)。それを行うことも、プログラミングの大きな課題です。
Fattie

超クール。でも質問があります。リクエスト3とリクエスト4が失敗した場合(サーバーエラー、認証エラーなど)、残りのリクエスト(リクエスト3とリクエスト4)のみに対してループを再度呼び出すにはどうすればよいですか?
JD。

43

Xcode 8.3.1-Swift 3

これは、Swift 3に変換されたpaulvsの受け入れられた回答です。

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 2017

@ lopes710を2番目に実行します-これは、すべてのリクエストが並行して動作することを許可するように見えますよね?
Chris Prince

forループ内に2つのネットワーク要求があり、一方が他方と入れ子になっている場合、forループの反復ごとに両方の要求が完了していることを確認する方法。?
Awais Fayyaz 2018

@チャンネル、これを注文する方法はありますか?
イスラエルメシレヤ

41

Swift 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サーバーに送信する必要があります。これには、最初のログインも含まれます。このアプローチにより、アプリは、最初のファイルをアップロードする前に一度だけログインすることが保証されます。複数回実行するのではなく、基本的に同時に(「順序なし」アプローチのように)、エラーが発生します。ありがとう!
Neph

でも、質問が1つあります。「?」をdispatchSemaphore.signal()去る前にするか後にするdispatchGroupかは重要ですか。セマフォのブロックをできるだけ遅く解除するのが最善だと思うかもしれませんが、グループを離れることがそれを妨害するかどうか、またどのように妨害するかはわかりません。私は両方の注文をテストしましたが、違いはないようでした。
Neph

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.