如何限制for循环中的网络调用次数?

Posted

技术标签:

【中文标题】如何限制for循环中的网络调用次数?【英文标题】:How to limit the number of network calls in a for loop? 【发布时间】:2020-01-18 15:38:16 【问题描述】:

我在服务器上为每个用户提供了一些对象,我必须全部获取它们。首先,我从服务器获取对象计数,以计算我必须分批进行的网络调用次数 30。假设有 2000 个对象,那么我必须进行的调用次数为 2000/30 = 67。我为第一次调用设置偏移量 = 0 和限制 = 29,然后将偏移量和限制都增加 30 并进行第二次调用,依此类推。问题是,如果我在 for 循环中同时发送超过 8 个调用,那么调用开始失败,即使我重试它仍然失败。我正在使用 Alamofire 向服务器发送发布请求。我注意到,如果我并行发送 8 个请求,一切似乎都很好。如何确保并行进行 8 个调用,当所有 8 个调用都完成后,再向服务器发送另外 8 个调用。

这是一段代码:

 let count = Double(totalTransactionsCount)
  //The maximum amount of transactions to fetch in one call.
  //let maxLimit = 30
  let calls = Int(ceil(count/Double(maxLimit)))

 for call in 1...calls 
        print("Calling call \(call) with offset:\(offset) to limit:\(limit)")
        callFetchTransactionWith(offset, limit)

        offset += maxLimit
        limit += maxLimit
    


 fileprivate func callFetchTransactionWith(_ offset: Int, _ limit: Int, _ callCountPercentage: Double, _ calls: Int) 
    TransactionsModel.reportTransactions(offset: offset, limit: limit ,success:  (transactions) in
        ACTIVITYCALL += 1
        self.currentSyncingProgress = self.currentSyncingProgress + CGFloat(callCountPercentage)
        Utility.logger(log: "\(self.currentSyncingProgress)", message: "Activity Increment Progress")
        if ACTIVITYCALL == calls 
            TransactionsModel.assignSequenceNumbers()
            self.didCompleteProgressAndSync(duration: 2.0)
            return
         else 
            self.updateProgressBar(value: self.currentSyncingProgress)
        
    , failure:  (response, statusCode) in
        print(response,statusCode)
        self.callFetchTransactionWith(offset, limit, callCountPercentage, calls)
    )



static func reportTransactions(offset:Int,limit:Int,
    success:@escaping ( _ model: [TransactionsModel] ) -> Void,
    failure:@escaping APIClient.FailureHandler) 

    let params:Parameters = [
        "offset":offset,
        "limit":limit
    ]

    let headers:[String:String] = [
        "X-Auth-Token":Singleton.shared.token
    ]

    if !Connectivity.isConnectedToInternet 
        Constants.UIWindow?.showErrorHud(text: AppString.internetUnreachable)
        return
    

    APIClient.shared().requestPost(endpoint: Route.reportTransactions, params: params, headers: headers, success:  (response) in
        guard
            let data = response as? [String:Any],
            let transactions = data["transactions"] as? [[String : Any]]
            else return

        let transactionModels = Mapper<TransactionsModel>().mapArray(JSONArray: transactions)
        TransactionsModel.save(transactions: transactionModels)

        success(transactionModels)
    )  (response, status) in
        print(response,status)
        failure(response,status)
    

【问题讨论】:

【参考方案1】:

信号量

这看起来是一个很好的信号量用例。

致谢:https://unsplash.com/photos/5F04PN6oWeM

让我向您展示如何使用 Playground 执行此操作,以便您可以在本地运行此解决方案,然后将其导入到您的项目中。

游乐场

首先创建一个新的空 Playground 页面和这 3 行,以便导入所需的库并启用并发。

import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

一旦您决定将此解决方案移动到您的项目中,您只需要 import Foundation 行。

目录

现在让我们定义以下 3 个常量。

并发调用 这是您要执行的并发调用数。

let concurrentCalls = 8

信号量 该信号量将允许执行不超过 8 个线程。当第 9 个线程请求访问时,它会处于等待状态,直到正在运行的 8 个线程之一完成。

let semaphore = DispatchSemaphore(value: concurrentCalls)

后台队列 我们将使用此队列异步调度所有调用。

let backgroundQueue = DispatchQueue(label: "Background queue")

fetchData(完成:

此函数模拟您的远程 API 调用。它只是等待 3 秒,然后通过代表结果的“?”字符串调用完成。

func fetchData(completion: @escaping (String) -> Void) 
    DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) 
        completion("?")
    

fetchAll(完成:)

现在我们处于解决方案的核心。

func fetchAll(completion: @escaping ([String]) -> Void) 
    // 1
    let storageQueue = DispatchQueue(label: "Serial queue")

    var results = [String]()
    let totalNumberOrCalls = 20

    for i in 0..<totalNumberOrCalls 
        backgroundQueue.async 
            semaphore.wait()
            fetchData  result in
                storageQueue.async 
                    results.append(result)
                    if i == totalNumberOrCalls - 1 
                        completion(results)
                    
                
                // 2
                semaphore.signal()
            
        
    

它是如何工作的?

我们有一个值设置为 8 的信号量。

每次我们想要执行网络调用时,我们都会询问信号量是否可以开始调用

// 1
semaphore.wait()

如果信号量的值大于 0,则它允许我们的远程调用并减少它的值。

否则,如果信号量保持为 0,则网络调用未执行,而是将其置于 wait 直到之前的调用之一结束。

一旦网络调用结束,我们就会调用

// 2
semaphore.signal

这样,信号量值增加 1,并允许执行另一个等待调用。

测试

现在我们调用调用

fetchAll  results in
    print(results)

fetchData 的并发调用不会超过 8 个,一旦所有调用完成,就会打印结果

["?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?"]

结论

希望这会有所帮助,如果您想了解更多详细信息,请查看我谈论信号量的这个答案https://***.com/a/51816661/1761687

【讨论】:

if i == totalNumberOrCalls - 1 。给定的结果可能是错误的,因为与其他任务相比,它可能是一个非常短的任务,它将在其他任务完成之前执行(除非信号量的值为 1)。为了防止您必须对已执行的任务进行计数器,当counter == totalNumberOfCalls - 1 时,我们才会到达最后一个

以上是关于如何限制for循环中的网络调用次数?的主要内容,如果未能解决你的问题,请参考以下文章

foreach循环怎么限制它循环次数,这个循环出8条,但是我想要4条,怎么改下代码?

如何确定for循环的次数!

如何确定for循环的次数

java 超多次数的for循环如何改善

Python中的嵌套循环

For 循环的迭代次数少于我在 Python 中的预期