Đợi cho đến khi swift cho vòng lặp với các yêu cầu mạng không đồng bộ kết thúc thực thi


159

Tôi muốn một vòng lặp for gửi đi một loạt các yêu cầu mạng đến căn cứ hỏa lực, sau đó chuyển dữ liệu sang bộ điều khiển khung nhìn mới sau khi phương thức kết thúc thực thi. Đây là mã của tôi:

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 

Tôi có một vài mối quan tâm. Đầu tiên, làm thế nào để tôi đợi cho đến khi vòng lặp for kết thúc và tất cả các yêu cầu mạng đã hoàn thành? Tôi không thể sửa đổi hàm obsSingleEventOfType, đây là một phần của SDK firebase. Ngoài ra, tôi sẽ tạo ra một số điều kiện cuộc đua bằng cách cố gắng truy cập dateArray từ các lần lặp khác nhau của vòng lặp for (hy vọng điều đó có ý nghĩa)? Tôi đã đọc về GCD và NSOperation nhưng tôi hơi lạc lõng vì đây là ứng dụng đầu tiên tôi xây dựng.

Lưu ý: Mảng vị trí là một mảng chứa các khóa tôi cần truy cập trong firebase. Ngoài ra, điều quan trọng là các yêu cầu mạng bị loại bỏ không đồng bộ. Tôi chỉ muốn đợi cho đến khi TẤT CẢ các yêu cầu không đồng bộ hoàn thành trước khi tôi chuyển dateArray cho bộ điều khiển xem tiếp theo.

Câu trả lời:


338

Bạn có thể sử dụng các nhóm điều phối để thực hiện một cuộc gọi lại không đồng bộ khi tất cả các yêu cầu của bạn kết thúc.

Dưới đây là một ví dụ sử dụng các nhóm điều phối để thực hiện gọi lại không đồng bộ khi nhiều yêu cầu kết nối mạng đã kết thúc.

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

Đầu ra

Finished request 1
Finished request 0
Finished request 2
Finished request 3
Finished request 4
Finished all requests.

Điều này đã làm việc tuyệt vời! Cảm ơn! Bạn có biết nếu tôi sẽ chạy vào bất kỳ điều kiện cuộc đua nào khi tôi đang cố gắng cập nhật dateArray không?
Josh

Tôi không nghĩ rằng có một điều kiện cuộc đua ở đây vì tất cả các yêu cầu đều thêm giá trị vào datesArrayviệc sử dụng một khóa khác.
paulvs

1
@Josh Về điều kiện cuộc đua: một điều kiện cuộc đua xảy ra, nếu cùng một vị trí bộ nhớ sẽ được truy cập từ các luồng khác nhau, trong đó ít nhất một truy cập là ghi - mà không sử dụng đồng bộ hóa. Tuy nhiên, tất cả các truy cập trong cùng một hàng đợi công văn nối tiếp được đồng bộ hóa. Đồng bộ hóa cũng xảy ra với các hoạt động bộ nhớ xảy ra trên hàng đợi A, gửi đến hàng đợi công văn khác B. Tất cả các hoạt động trong hàng đợi A sau đó được đồng bộ hóa trong hàng đợi B. Vì vậy, nếu bạn nhìn vào giải pháp, nó không tự động đảm bảo rằng các truy cập được đồng bộ hóa. ;)
CouchDeveloper 28/03/2016

@josh, lưu ý rằng "lập trình đường đua", nói một cách khó khăn là rất khó. Không bao giờ có thể ngay lập tức nói "bạn làm / không có vấn đề ở đó." Đối với các lập trình viên có sở thích: "đơn giản" luôn hoạt động theo cách có nghĩa là các vấn đề về đường đua, đơn giản là không thể. (Ví dụ: những việc như "chỉ làm một việc một lúc", v.v.) Ngay cả khi làm điều đó cũng là một thách thức lớn về lập trình.
Fattie

Siêu mát. Nhưng tôi có một câu hỏi. Giả sử yêu cầu 3 và yêu cầu 4 không thành công (ví dụ lỗi máy chủ, lỗi ủy quyền, bất cứ điều gì), sau đó làm thế nào để gọi lại vòng lặp chỉ cho các yêu cầu còn lại (yêu cầu 3 & yêu cầu 4)?
JD.

43

Xcode 8.3.1 - Swift 3

Đây là câu trả lời được chấp nhận của paulvs, được chuyển đổi thành 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
Xin chào, cái này có hoạt động không khi cho phép những người khác nói 100 yêu cầu? hay 1000? Bởi vì tôi đang cố gắng thực hiện điều này với khoảng 100 yêu cầu và đang gặp sự cố khi hoàn thành yêu cầu.
lopes710

Tôi thứ hai @ lopes710-- Điều này dường như cho phép tất cả các yêu cầu hoạt động song song, phải không?
Hoàng tử Chris

Nếu tôi có 2 yêu cầu mạng, một yêu cầu lồng nhau, bên trong vòng lặp for, thì làm thế nào để đảm bảo rằng với mỗi lần lặp của vòng lặp for, cả hai yêu cầu đã được hoàn thành. ?
Awais Fayyaz

@ Kênh, xin vui lòng có cách nào để tôi có thể đặt hàng này?
Israel Meshileya

41

Swift 3 hoặc 4

Nếu bạn không quan tâm đến các đơn đặt hàng , hãy sử dụng câu trả lời của @ paulvs , nó hoạt động hoàn hảo.

khác chỉ trong trường hợp nếu bất cứ ai muốn nhận được kết quả theo thứ tự thay vì bắn chúng đồng thời, đây là mã.

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

Ứng dụng của tôi phải gửi nhiều tệp đến máy chủ FTP, bao gồm cả việc đăng nhập trước. Cách tiếp cận này đảm bảo rằng ứng dụng chỉ đăng nhập một lần (trước khi tải lên tệp đầu tiên), thay vì cố gắng thực hiện nhiều lần, tất cả cùng một lúc (như cách tiếp cận "không có thứ tự"), sẽ gây ra lỗi. Cảm ơn!
Cháu 5/12/19

Tôi đã có một câu hỏi mặc dù: Có vấn đề gì nếu bạn làm dispatchSemaphore.signal()trước hoặc sau khi rời khỏi dispatchGroup? Bạn sẽ nghĩ rằng tốt nhất là bỏ chặn semaphore càng muộn càng tốt nhưng tôi không chắc chắn nếu và làm thế nào để rời khỏi nhóm can thiệp vào điều đó. Tôi đã thử nghiệm cả hai đơn đặt hàng và nó dường như không tạo ra sự khác biệt.
Cháu 5/12/19

16

Chi tiết

  • Xcode 10.2.1 (10E1001), Swift 5

Giải pháp

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

Sử dụng

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

Mẫu đầy đủ

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

Bạn sẽ cần sử dụng semaphores cho mục đích này.

 //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: Bạn cũng có thể sử dụng semaphores theo cách này. Kết quả rất hữu ích, bên cạnh đó bạn có thể theo dõi chính xác thời điểm và quá trình hoàn thành. Điều này đã được trích xuất từ ​​mã của tôi:

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

Chúng ta có thể làm điều này với đệ quy. Lấy ý tưởng từ mã dưới đây:

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

Nhóm điều phối là tốt nhưng thứ tự của các yêu cầu được gửi là ngẫu nhiên.

Finished request 1
Finished request 0
Finished request 2

Trong trường hợp dự án của tôi, mỗi yêu cầu cần được khởi chạy là đúng thứ tự. Nếu điều này có thể giúp ai đó:

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

Gọi :

trySendRequestsNotSent()

Kết quả :

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

Xem thêm thông tin: Gist

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.