等待 swift for 循环与异步网络请求完成执行

我想要一个 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函数,它是firebase SDK的一部分。此外,我是否会通过尝试从for循环的不同迭代访问datesArray来创建某种竞争条件(希望有意义)?我一直在阅读关于GCD和NSOperation,但我有点迷失,因为这是我建立的第一个应用程序。

注意:位置数组是一个数组,其中包含我需要在firebase中访问的键。另外,网络请求是异步发出的,这一点很重要。我只是想等到所有的异步请求完成之前,我传递datesArray到下一个视图控制器。

114279 次浏览

您可以使用调度组在所有请求完成时触发异步回调。

下面是一个使用调度组在多个网络请求全部完成时异步执行回调的示例。

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.

为此,您需要使用信号量。

 //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: 您也可以这样使用信号量。它的结果非常有用,除此之外,您还可以准确跟踪完成的时间和过程。这已经从我的代码中提取:

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

Xcode 8.3.1 - Swift 3

这是paulvs的公认答案,转换为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.")
})
}

调度组很好,但是发送请求的顺序是随机的。

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

更多信息: 要点 < / p >

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

细节

  • Xcode 10.2.1 (10E1001)

解决方案

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() }
}
}
我们可以用递归来做。 从下面的代码:

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

iOS 15+ (Swift 5.5)更新

我为Swift 5.5和iOS 15+添加了一个更现代的解决方案,因为这个工具链包括主要的URLSession API改进,而不是特定于Firebase或Alamofire。代码使用async / await,即Structured Concurrency。这是苹果在最新iOS版本(iOS 13.0+)上推荐的并发请求。

我们现在用更少的代码行和更多的自定义实现了与DispatchGroups相同的结果。这个答案将帮助那些曾经排队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
}

它还使用for await循环(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并不总是允许请求一到达就被处理。

这种方法的好处是代码更干净,更容易编写,更安全,并避免死锁/线程问题。

请注意

TaskGroupasync let的确切语法将来可能会改变。目前,结构化并发在早期版本中有了很大的改进,现在可以稳定地用于生产。

苹果已经澄清,分组和异步任务的底层机制基本上已经敲定(在Swift Evolution中得到批准)。一些语法变化的例子已经包括用Task {替换async {

在最初的问题中,考虑了如何知道所有查询何时完成,并在字典中返回结果。Paulvs已经回答了(+1)那个问题:如果使用旧的完成处理程序闭包模式,使用分派组来知道它们什么时候完成。现在,如果使用Swift并发,则使用模式由Pranav Kasetti提出

但是,如果需要有序数组中的结果,则不应该让请求本身按顺序运行。如果您这样做,您将付出严重的性能代价(通常比原来慢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] }
}
}

但是,即使您使用的是较旧的完成处理程序闭包模式,其思想也是相同的:将结果存储在字典中,享受并发性,并在确实需要时在末尾构建排序数组。