循环调用带完成回调的函数时如何保证结果顺序
循环调用带完成回调的函数时如何保证结果顺序
嘿,这个问题我之前做异步任务的时候也踩过坑!你碰到的核心问题就是异步回调的执行顺序不固定,但我们需要把结果对应到原来的调用顺序上对吧?其实你的实验已经摸到门道了——闭包会捕获当前作用域里的值类型变量的当前值,而不是引用,这正是我们解决问题的关键!
最优方案:给每个异步调用标记顺序索引
我们可以在循环的时候记录每个调用的位置(索引),把这个索引和回调绑定,这样不管回调什么时候回来,都能把结果放到数组的正确位置上,而且还能保留并行调用的效率。
修改你的retrieveAllWords方法就行:
func retrieveAllWords() { // 先初始化数组,预留好所有位置,避免append导致的顺序混乱 words = Array(repeating: "", count: 21) // 0...20一共21个元素 for index in 0...20 { findNextWord { [weak self] word in guard let self = self else { return } // 直接把结果放到对应索引的位置,不管回调顺序如何 self.words[index] = word } } }
为啥这个能行?就像你实验里的a_not_self一样,循环里的index是每次迭代的局部变量,闭包会捕获它当时的具体值——第一个循环捕获的是0,第二个是1,以此类推。所以每个回调回来时,都知道自己该放到数组的哪个位置,完美解决顺序问题!
备选方案:串行执行异步调用(适合必须顺序执行的场景)
如果你的业务逻辑要求必须等前一个findNextWord完成后再调用下一个,那可以用串行队列+DispatchGroup来强制顺序执行,不过这样会丢失并行调用的效率:
func retrieveAllWords() { let serialQueue = DispatchQueue(label: "com.example.wordSerialQueue") serialQueue.async { [weak self] in guard let self = self else { return } for _ in 0...20 { let group = DispatchGroup() group.enter() self.findNextWord { word in self.words.append(word) group.leave() } group.wait() // 等待当前异步调用完成,再进行下一次循环 } } }
这种方法的好处是完全不用管顺序,因为调用本身就是串行的,但代价是所有异步任务变成了串行执行,速度会慢很多,所以只在必须依赖前一个结果的场景下用。
再聊下你的实验发现
你实验里的a_not_self和self?.a的差异,就是闭包的值捕获特性:a_not_self是值类型(Int),闭包捕获的是它当时的副本;而self?.a是引用类型的属性,闭包捕获的是self的引用,所以取到的是最新值。这也是为什么我们能用索引法的核心原因——每个闭包里的索引都是当时循环的那个固定值。
备注:内容来源于stack exchange,提问作者sudocu




