DispatchQueue.asyncAfter没有像我想象的那样工作?

时间:2017-02-26 17:57:16

标签: ios swift multithreading grand-central-dispatch

我正在处理一个应用程序,其中我尝试调试的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()函数修复了它,但我很好奇这在Swift中如何使用GCD。

谢谢!

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

3 个答案:

答案 0 :(得分:1)

考虑您的代码的作用。它运行一个循环,该循环接收数组中的每个项目,并为每个项目调用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()) } } } } 。 for循环几乎没有时间执行,因此每个项目都会从“现在4秒后运行”延迟。 (好吧,长列表中的最后一个可能会比第一个延迟一个微秒。)

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

DispatchQueue(label:)

请注意,您应该重新构建代码,以便在上一个任务完成之前不启动下一个请求,并跟踪发出每个请求的时间,计算您在最后一分钟发出的请求数,一旦达到阈值,等待发出下一个请求,直到最后一分钟发出的请求总数低于最大请求“年龄关闭”。

编辑:

重新阅读您的问题和评论之后,我会更好地了解您的目标。您正试图利用Data(contentsOf:)初始化程序默认为您提供串行队列这一事实。

问题是URLSession是一个异步框架。当您启动下载任务时,它会立即返回。因此,您的串行任务队列都可以非常快速地完成,但下载任务会在URLSession中堆叠并根据其计划运行。

如果要使用串行队列进行下载,可以使用同步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:)

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

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

您还可以使用def wrap(s, l): # How many characters to add to last element r = 10 - len(l[-1]) if l else 0 # Augment last string if r > 0: l[-1] += s[:r] # Add remaining chunks of length 10 for i in range(r, len(s), 10): l.append(s[i:i + 10]) return l l = ["12345678", "123456789"] print(wrap("a", l)) print(wrap("123456789123456789123", l)) 完成上述操作,具有更大的灵活性和控制力。

答案 1 :(得分: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的内部队列或网络稳定性)来维护适当的时机,不会牺牲URLSession的现代特征。

答案 2 :(得分:0)

你的问题是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())
    }
}