等到带有异步网络请求的 swift for 循环完成执行
Posted
技术标签:
【中文标题】等到带有异步网络请求的 swift for 循环完成执行【英文标题】:Wait until swift for loop with asynchronous network requests finishes executing 【发布时间】:2016-06-24 17:05:05 【问题描述】:我想要一个 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 的信息,但我有点迷茫,因为这是我构建的第一个应用程序。
注意:Locations 数组是一个包含我需要在 firebase 中访问的键的数组。此外,异步触发网络请求也很重要。我只想等到所有异步请求完成后再将 datesArray 传递给下一个视图控制器。
【问题讨论】:
【参考方案1】:您可以使用dispatch groups 在所有请求完成后触发异步回调。
这是一个使用调度组在多个网络请求全部完成时异步执行回调的示例。
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 时是否会遇到任何竞争条件吗? 我认为这里没有竞争条件,因为所有请求都使用不同的键向datesArray
添加值。
@Josh 关于竞争条件:如果从不同线程访问相同的内存位置,则发生竞争条件,其中至少一个访问是写入 - 没有使用同步.但是,同一个串行调度队列中的所有访问都是同步的。同步也发生在调度队列 A 上发生的内存操作,该调度队列 A 提交到另一个调度队列 B。队列 A 中的所有操作然后在队列 B 中同步。因此,如果您查看解决方案,并不能自动保证访问是同步的。 ;)
@josh,请注意“赛道编程”总之是非常困难的。永远不可能立即说“你有/没有问题”。对于业余程序员:“简单”总是以一种意味着赛道问题是不可能的方式工作。 (例如,“一次只做一件事”等)即使这样做也是一个巨大的编程挑战。
超级酷。但我有一个问题。假设请求 3 和请求 4 失败(例如服务器错误、授权错误等),那么如何只为剩余的请求(请求 3 和请求 4)再次调用 for 循环?【参考方案2】:
您需要为此使用信号量。
//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】:斯威夫特 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...
【讨论】:
【参考方案4】: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.")
)
【讨论】:
嗨,这是否适用于假设 100 个请求?还是1000?因为我正在尝试使用大约 100 个请求来执行此操作,并且在请求完成时崩溃。 我第二个 @lopes710-- 这似乎允许所有请求并行操作,对吧? 如果我有 2 个网络请求,一个与另一个嵌套在一个 for 循环中,那么如何确保对于 for 循环的每次迭代,两个请求都已完成。 ? @Channel ,请问有什么方法可以订购吗?【参考方案5】:调度组不错,但是发送请求的顺序是随机的。
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
【讨论】:
【参考方案6】:Swift 3 或 4
如果您不关心订单,请使用@paulvs 的answer,它非常有效。
如果有人想按顺序获取结果而不是同时触发它们,则here 是代码。
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 服务器,这也包括首先登录。这种方法保证应用程序只登录一次(在上传第一个文件之前),而不是尝试多次登录,基本上在同一时间(如“无序”方法),这会触发错误。谢谢! 不过我有一个问题:在离开dispatchGroup
之前还是之后,您是否执行dispatchSemaphore.signal()
是否重要?您会认为最好尽可能晚地解除信号量的阻塞,但我不确定离开组是否以及如何干扰这一点。我测试了两个订单,但似乎没有什么不同。【参考方案7】:
详情
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()
【讨论】:
【参考方案8】:我们可以通过递归来做到这一点。 从下面的代码中获取想法:
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()
【讨论】:
【参考方案9】:ios 15+ (Swift 5.5) 更新
由于这个问题不是针对 Firebase 或 Alamofire,我想为 Swift 5.5 和 iOS 15+ 添加更现代的解决方案。
下面的答案使用async / await
,即Structured Concurrency
。下面概述的方法是 Apple 推荐的对最新 iOS 版本 (13+) 并发请求的方法。
这个答案将帮助那些曾经排队 URLSession
请求并等待这些请求完成的用户。
任务组示例代码
如果我们有一个动态数量的请求(可变大小的数组),那么正确的工具是Task
Group。
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
示例代码
此语法适用于固定数量的请求。
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
的确切语法将来可能会发生变化。目前,结构化并发在其早期版本中正在改进并存在一些错误。
但是,Apple 已经明确表示,分组和异步任务的底层机制大多已完成(在 Swift Evolution 中获得批准)。一些语法更改的示例已经包括将async
替换为Task
。
【讨论】:
以上是关于等到带有异步网络请求的 swift for 循环完成执行的主要内容,如果未能解决你的问题,请参考以下文章