비동기 네트워크 요청으로 신속한 루프 실행이 완료 될 때까지 기다립니다.


159

for 루프에서 많은 네트워크 요청을 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 함수를 수정할 수 없으며 firebase SDK의 일부입니다. 또한 for 루프의 다른 반복에서 datesArray에 액세스하여 일종의 경쟁 조건을 만들 것입니까? 나는 GCD와 NSOperation에 대해 읽었지만 이것이 내가 만든 첫 번째 앱이므로 약간 잃어 버렸습니다.

참고 : 위치 배열은 firebase에서 액세스해야하는 키가 포함 된 배열입니다. 또한 네트워크 요청이 비동기 적으로 시작되는 것이 중요합니다. 날짜 배열을 다음보기 컨트롤러에 전달하기 전에 모든 비동기 요청이 완료 될 때까지 기다립니다.

답변:


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

1
@Josh 경쟁 조건 : 동기화 사용 하지 않고 하나 이상의 액세스가 쓰기 인 다른 스레드에서 동일한 메모리 위치에 액세스 할 경우 경쟁 조건이 발생합니다 . 그러나 동일한 직렬 디스패치 큐 내의 모든 액세스는 동기화됩니다. 다른 디스패치 큐 B에 제출하는 디스패치 큐 A에서 발생하는 메모리 조작으로도 동기화가 발생합니다. 그러면 큐 A의 모든 조작이 큐 B에서 동기화됩니다. 따라서 솔루션을 보면 액세스가 동기화되는 것이 자동으로 보장되지는 않습니다. ;)
CouchDeveloper

@josh, 한마디로 "racetrack programming"은 엄청나게 어렵다. "문제가 없다"고 즉시 말할 수는 없습니다. 애호가 프로그래머의 경우 : "간단히"항상 경마장 문제가 불가능하다는 의미로 작동합니다. (예를 들어, "한 번에 한 가지 작업 만 수행"등과 같은 작업이 가능합니다.) 그렇게하는 것은 엄청난 프로그래밍 과제입니다.
Fattie

슈퍼 쿨. 하지만 질문이 있습니다. 요청 3과 요청 4가 실패했다고 가정하면 (예 : 서버 오류, 권한 부여 오류 등) 나머지 요청에 대해서만 루프를 다시 호출하는 방법 (요청 3 및 요청 4)?
JD.

43

Xcode 8.3.1-스위프트 3

이것은 스위프트 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

두 번째 @ lopes710-모든 요청이 병렬로 작동하는 것처럼 보입니다.
크리스 프린스

두 개의 네트워크 요청이 있고 하나는 for 루프 내부에 중첩되어 있고 for 루프를 반복 할 때마다 두 요청이 모두 완료되었는지 확인하는 방법이 있습니다. ?
Awais Fayyaz

@Channel, 주문할 수있는 방법이 있습니까?
이스라엘 Meshileya

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 서버로 보내야하며 여기에는 먼저 로그인이 포함됩니다. 이 접근 방식은 앱이 여러 번 시도하는 대신 기본적으로 동시에 ( "정렬되지 않은"접근 방식과 같이) 여러 번 시도하지 않고 한 번만 (첫 번째 파일을 업로드하기 전에) 로그인하여 오류를 유발할 수 있도록합니다. 감사!
Neph

그래도 한 가지 질문 dispatchSemaphore.signal()이 있습니다 dispatchGroup. 가능한 한 늦게 세마포어를 차단 해제하는 것이 가장 좋다고 생각하지만 그룹을 떠나는 것이 어떻게 방해하는지 확실하지 않습니다. 두 주문을 모두 테스트했지만 차이가없는 것 같습니다.
Neph

16

세부

  • Xcode 10.2.1 (10E1001), 스위프트 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.