programing

비동기 네트워크 요청이 포함된 루프의 실행이 완료될 때까지 기다립니다.

instargram 2023. 4. 22. 08:26
반응형

비동기 네트워크 요청이 포함된 루프의 실행이 완료될 때까지 기다립니다.

for in loop을 통해 다수의 네트워크 요구를 파이어베이스로 전송하고 메서드의 실행이 완료되면 새로운 뷰 컨트롤러로 데이터를 전달하고 싶습니다.코드는 다음과 같습니다.

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 루프가 종료되고 모든 네트워크 요구가 완료될 때까지 어떻게 기다려야 합니까?firebase SDK의 일부이기 때문에 severSingleEventOfType 함수는 수정할 수 없습니다.또한 for loop의 여러 반복에서 datesArray에 액세스하여 일종의 레이스 조건을 만들 수 있습니까?GCD와 NSOperation에 대해 읽고 있습니다만, 처음 만든 앱이라 조금 당황하고 있습니다.

메모: Locations Array는 FireBase에서 액세스하기 위해 필요한 키를 포함하는 배열입니다.또, 네트워크 요구를 비동기적으로 기동하는 것도 중요합니다.모든 비동기 요청이 완료될 때까지 기다린 후 datesArray를 다음 뷰 컨트롤러로 전달합니다.

디스패치 그룹을 사용하여 모든 요구가 종료되었을 때 비동기 콜백을 실행할 수 있습니다.

다음 예제에서는 여러 네트워킹 요청이 모두 완료된 경우 디스패치 그룹을 사용하여 콜백을 비동기적으로 실행하는 방법을 보여 줍니다.

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.

스위프트 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()
        }
    }
}

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

세부 사항

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

iOS 15+ 업데이트 (Swift 5.5)

툴 Swift 5.5 및iOS 15+가 포함되어 있기 때문에 .5 및에 대해 보다 URLSessionFirebase "Alarmofire" "API"에는 '보다 낫다'가 사용되고 있습니다.async / await ㅇㅇㅇ.Structured Concurrency최신 iOS 버전(iOS 13.0+)의 동시 요청에 대해 애플이 권장하는 것입니다.

해서 과 같은 결과를 수 있습니다.DispatchGroup코드 행이 적어지고 커스터마이즈도 많아집니다.이 됩니다.URLSession을 사용법

작업 그룹 예제 코드

은 ★★★★★★★★★★★★★★★★★★★★★★」TaskGroup동동표표표 ( 표표표가가가가가가가 ) 。

func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
  var thumbnails: [String: UIImage] = [:]
  try await withThrowingTaskGroup(of: (String, UIImage).self) { group in
    for id in ids {
      group.addTask {
        return (id, try await fetchOneThumbnail(withID: id))
      }
    }
    for try await (id, thumbnail) in group {
      thumbnails[id] = thumbnail
    }
  }
  return thumbnails
}


func fetchOneThumbnail(withID id: String) async throws -> UIImage {
    // Just for demo purpose. In PROD, we may use dynamic URLs for each ID.
    guard let url = URL(string: "http://placekitten.com/200/300") else {
        throw ThumbnailError.invalidURL
    }
    // I have used `data(from: URL, delegate: URLSessionTaskDelegate? = nil)`
    // but we can also use `data(for: URLRequest, delegate: URLSessionTaskDelegate? = nil)`)`.
    // If we want to observe the delegate changes like when the
    // request fails, completes, or redirects, use the delegate param.
    // e.g. try await URLSession.shared.data(from: url, delegate: delegate)
    let result: (data: Data, response: URLResponse) = try await URLSession.shared.data(from: url)
    guard let image = UIImage(data: result.data) else {
        throw ThumbnailError.missingImageData
    }
    return image
}

enum ThumbnailError: Error {
    case invalidURL
    case missingImageData
}

Task {
    let images = try await fetchThumbnails(for: ["1", "2", "3"])
    // Show thumbnails in UI.
}

, 「」를 합니다.for awaitsloope(루프)AsyncSequence작업 완료를 기다립니다. for try await입니다.AsyncSequence. 구문이 있기 URLSession.data(for:)메서드 패밀리는 던지기 기능입니다.

async let

async let구문은 고정된 수의 요청에 대해 작동합니다.

let reqOne = urlRequest(for: keyOne) // Function that returns a unique URLRequest object for this key. i.e. different URLs or format.
async let (dataOne, _) = URLSession.shared.data(for: reqOne)

let reqTwo = urlRequest(for: keyTwo)
async let (dataTwo, _) = URLSession.shared.data(for: reqTwo)

guard let parsedData = parseInformation(from: try? await dataOne) else {
    // Call function to parse image, text or content from data.
      continue 
}
// Act on parsed data if needed.

guard let parsedDataTwo = parseInformation(from: try? await dataTwo) else {
    // Call function to parse image, text or content from data.
      continue 
}
// Act on the second requests parsed data if needed.

// Here, we know that the queued requests have all completed.

「」를 참조할 수 없습니다.await 것을 ''이라고 합니다.async let.

이 코드 예는 가변 크기 어레이에 적용할 수 있지만 Apple에서는 권장하지 않습니다.그 이유는async let요청이 도착하자마자 처리되는 것을 항상 허용하지는 않습니다.

이 접근법의 장점은 쓰기 쉽고, 안전하며, 교착 상태/스레딩 문제를 방지할 수 있는 보다 깔끔한 코드입니다.

메모

의 .TaskGroup ★★★★★★★★★★★★★★★★★」async let츠미야현재 Structured Concurrency는 초기 출시 기간 동안 크게 개선되었으며 현재는 생산에 안정적입니다.

Apple은 그룹화 태스크와 비동기 태스크의 기본 메커니즘은 대부분 완료(Swift Evolution에서 승인됨)되어 있음을 명확히 했습니다. "예"의 "예"를 "예"는 "예"입니다.async {Task {.

이를 위해서는 세마포를 사용해야 합니다.

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

재귀로 할 수 있어요아래 코드에서 아이디어를 얻습니다.

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

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

원래 질문에서는 모든 쿼리가 언제 완료되었는지 알고 결과를 사전에 반환하는 방법을 고려했습니다. paulvs는 다음과 같은 질문에 대해 (+1) 대답했습니다.이전 완료 핸들러 종료 패턴을 사용하는 경우 디스패치 그룹을 사용하여 완료 시간을 확인합니다.그리고 오늘날 스위프트 동시성을 사용하는 경우 프라나브 카세티가 제안한 패턴을 사용합니다.

단, 순서가 지정된 배열에서 결과가 필요한 경우 요청 자체를 순차적으로 실행해서는 안 됩니다.그렇게 하면 심각한 성능 패널티(대개 3배 이상 느림)를 지불해야 합니다.세마포어를 사용하여 이를 달성하면 다른 모든 종류의 비효율성과 교착 상태의 위험이 발생합니다.

대신 정렬된 배열에 결과가 꼭 필요한 경우 paulvs의 답변을 사용하여 동시 요청에서 사전을 채우고 마지막에 결과 배열을 구축해야 합니다.예.,

let array = ids.compactMap { resultsDictionary[$0] }

예를 들어 Swift 동시성은 이 모든 것을 매우 적절하게 처리합니다.


func fetch(for ids: [Id]) async throws -> [Foo] {
    try await withThrowingTaskGroup(of: (Id, Foo).self) { [self] group in
        for id in ids {
            group.addTask { (id, try await fetch(for: id)) }
        }
        
        let dictionary = try await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
        return ids.compactMap { dictionary[$0] }
    }
}

그러나 오래된 완료 핸들러 종료 패턴을 사용하고 있는 경우에도 생각은 동일합니다.결과를 사전에 저장하고, 동시성을 즐기며, 필요한 경우 마지막에 정렬된 배열을 구축할 수 있습니다.

디스패치 그룹은 정상이지만 전송된 요청의 순서가 랜덤입니다.

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

자세한 것은, 을 참조해 주세요.

언급URL : https://stackoverflow.com/questions/35906568/wait-until-swift-for-loop-with-asynchronous-network-requests-finishes-executing

반응형