【问题标题】:DispatchQueue.asyncAfter doesn't work the way I thought it would?DispatchQueue.asyncAfter 不像我想象的那样工作?
【发布时间】:2017-02-27 11:58:35
【问题描述】:

我正在开发一个应用程序,其中我尝试调试的 403 禁止错误实际上是由每分钟可以向端点发出的请求数量的上限引起的(我真傻)。

很好,我决定将我的网络请求放到 DispatchQueue 上(无论如何更好的并发设计)并使用 asyncAfter(deadline:execute:) 函数在每个请求之间造成 4 秒的延迟。

我的程序的设计是列表中的每个项目调用一个函数,并且在该函数中,一些工作(请求)被放入该调度队列。见下文:

class ViewController

let serialQueue = DispatchQueue(label: "networkRequests")

func myFirstFunc {
    for item in items {
        self.mySecondFunc(item: item, completionHandler: {(completionItem) in
            // you shouldn't need this
       })
    }
}

func mySecondFunc(item: someType, completionHandler: @escaping (String?) -> Void) {

        let task = session.dataTask(with: request, completionHandler: {(data, response, error) in
            // stuff 
            completionHandler(changedItem)
        })
        self.serialQueue.asyncAfter(deadline: .now() + 4.0) {
                task.resume()
                print(Date())
            }
        }
    }
}

我认为这会起作用的方式是,无论该函数是否被不同的线程同时调用,包含在 asyncAfter(deadline:execute:) 调用中的代码都会排队,并且下一个闭包不会开始执行直到最后一次关闭完成,又过了 4 秒。

这不起作用 - print(Date()) 在打印时间之间没有延迟。

我通过在 unix 中使用 sleep() 函数来修复它,但我很好奇它在使用 GCD 的 Swift 中如何工作。

谢谢!

编辑:专门寻找具有功能的正确方法,其中单个线程执行每个请求,以便线程被阻塞,直到前一个线程完成,中间有 4 秒的延迟。

【问题讨论】:

    标签: ios swift multithreading grand-central-dispatch


    【解决方案1】:

    此处列出的其他答案是有效的,但它们并不像应有的那样可靠。以固定的时间间隔调用task.resume() 并不能保证实际以这些时间间隔发送请求。您受URLSession.shared 维护的内部队列的支配。每隔 4 秒添加任务并不意味着它们一定会在那个时间内发送。它也不保证请求需要多长时间(想想服务质量差的移动网络)。至于Data(contentsOf:),它绝对没有提供真正的功能或自定义,例如错误处理。

    更强大的解决方案是使用 DispatchGroup 并仅在前一个请求完成 4 秒后发起新请求。

    class ViewController
    
    let serialQueue = DispatchQueue(label: "networkRequests")
    let networkGroup = DispatchGroup()
    
    func myFirstFunc {
        for item in items {
            self.mySecondFunc(item: item, completionHandler: {(completionItem) in
                // you shouldn't need this
           })
        }
    }
    
    func mySecondFunc(item: someType, completionHandler: @escaping (String?) -> Void) {
    
            let task = session.dataTask(with: request, completionHandler: {(data, response, error) in
                // stuff 
                completionHandler(changedItem)
    
                Thread.sleep(forTimeInterval: 4) // Wait for 4 seconds
                networkGroup.leave() // Then leave this block so the next one can run
            })
            self.networkGroup.notify(queue: serialQueue) {
                networkGroup.wait() // Wait for the previous block to finish
                networkGroup.enter() // Enter a new block
                task.resume()
                print(Date())
            }
        }
    }
    

    这将保证每个后续请求在前一个请求完成后不早于 4 秒发送,并且不依赖外部因素(如 URLSession 的内部队列或网络稳定性)来保持适当的时间,而不会牺牲URLSessions 的现代特色。

    【讨论】:

    • 非常有帮助的答案,@Garrett。困扰我的一个部分是对Thread.sleep 的调用,它阻塞了当前线程。我建议改为DispatchQueue.global().asyncAfter(wallDeadline: DispatchWallTime.now() + 4.0) { self.group.leave() }。
    • 好点是不能保证以特定顺序发出网络调用意味着它们实际上会按该顺序执行。您不仅回答了所提出的问题,还解决了问题的根本问题。 (已投票)
    【解决方案2】:

    想想你的代码做了什么。它通过一个循环来获取数组中的每个项目,并为每个项目调用asyncAfter()。 for 循环的执行时间几乎为零,因此每个项目都获得相同的“从现在开始运行 4 秒”延迟。 (好吧,长列表中的最后一个可能比第一个延迟延迟一微秒。)

    如果您希望每个请求在前一个请求开始后 4 秒运行,则需要增加请求之间的延迟:

    class ViewController
    
    let serialQueue = DispatchQueue(label: "networkRequests")
    
    func myFirstFunc {
        for (index, item) in items.enumerated {
            self.mySecondFunc(item: item, 
              index: index, 
              completionHandler: {(completionItem) in
                // you shouldn't need this
           })
        }
    }
    
    func mySecondFunc(
      item: someType, 
      index: Int,
      completionHandler: @escaping (String?) -> Void) {
    
            let task = session.dataTask(with: request, completionHandler: {(data, response, error) in
                // stuff 
                completionHandler(changedItem)
            })
            self.serialQueue.asyncAfter(deadline: .now() + 4.0 * index) {
                    task.resume()
                    print(Date())
                }
            }
        }
    }
    

    请注意,您确实应该重构您的代码,以便在上一个任务完成之前不触发下一个请求,并且还要跟踪您发出每个请求的时间,计算您在最后一分钟发出了多少个请求,一旦达到阈值,请等待发出下一个请求,直到最后一分钟发出的请求总数降至最大值以下,因为最旧的请求“老化”。

    编辑:

    在重新阅读您的问题和您的 cmets 后,我对您想要做什么有了更好的了解。您正在尝试利用 DispatchQueue(label:) 初始化程序默认为您提供串行队列这一事实。

    问题在于 URLSession 是一个异步框架。当您开始下载任务时,它会立即返回。因此,您的串行任务队列都很快完成,但下载任务在 URLSession 中堆叠并按照它的时间表运行。

    如果你想使用你的串行队列做下载,你可以使用同步Data(contentsOf:)初始化器来同步读取数据:

    let serialQueue = DispatchQueue(label: "networkRequests")
    
    func readDataItems {
        for item in items {
            serialQueue.async {
              let data = Data(contentsOf: item.url)
              //Process this data item
              sleep(4)
            }
        }
    }
    

    因为Data(contentsOf:) 函数是同步的,它会导致串行队列中的任务阻塞,直到它完成。然后您将任务休眠 4 秒钟,然后再继续下一个任务。

    正如我回答的第一部分所述,您应该真正跟踪每个任务完成的时间,并且只有在您没有超过允许的下载次数/分钟时才开始下载。

    您也可以使用OperationQueue 完成上述操作,具有更大的灵活性和控制力。

    【讨论】:

    • 这仍然有很大的缺陷。延迟确实需要在上一个请求实际完成后 4 秒。这个答案只是确保每个请求在前一个请求启动后 4 秒启动。由于网络问题,这仍然可能导致所有请求同时发生。
    • 是的,我在回答中说过。这回答了 OP 的问题,但仍然不是问题的完整解决方案。
    • @rmaddy,如果您觉得雄心勃勃,您应该实施适当的解决方案并将其作为更好的答案发布。这比我有时间做的工作要多。
    • @Duncan C 感谢您的解释。这很有意义。我认为 asyncAfter 的工作方式是将闭包放在队列中,如果它是一个串行队列,则在最后一个队列完成并且另外 4 秒过去之前不执行下一个队列。您将如何设计它,以便在同一个线程上调用每个 task.resume() + 等待上一个调用完成,然后再等待 4 秒,然后进行调用?在我看来,我正在寻找类似 syncAfter 的东西,所以线程会阻塞。不过,我不确定如何实现所需的功能。
    【解决方案3】:

    您的问题是 task.resume() 异步执行,因此 mySecondFunc 退出,您在几毫秒后开始处理下一个请求。在第一个请求最初的 4 秒延迟之后,这些请求最终被非常接近地发送。

    要解决这个问题,请在第二个函数中添加一个延迟参数:

    func myFirstFunc {
        for (index, item) in items.enumerated() {
            self.mySecondFunc(item: item, delay: index * 4) {completionItem in
                // do stuffs
            }
        }
    }
    
    func mySecondFunc(item: SomeType, delay: TimeInterval = 0, completionHandler: @escaping (String?) -> Void) {
        let task = session.dataTask(with: request) {data, response, error in
            // stuff
            completionHandler(changedItem)
        }
        self.serialQueue.asyncAfter(deadline: .now() + delay) {
            task.resume()
            print(Date())
        }
    }
    

    【讨论】:

    • 这仍然有很大的缺陷。延迟确实需要在上一个请求实际完成后 4 秒。这个答案只是确保每个请求在前一个请求启动后 4 秒启动。由于网络问题,这仍然可能导致所有请求同时发生。
    • @rmaddy 我同意这些要求可以解释:“在每个请求之间造成 4 秒的延迟”。我认为每个请求间隔 4 秒开始,而不是在前一个请求完成 4 秒后开始下一个请求。
    猜你喜欢
    • 2010-11-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-10
    • 2016-02-27
    • 2015-05-16
    • 1970-01-01
    相关资源
    最近更新 更多