关于Kingfisher中ImageDownloader的GCD并发屏障队列使用疑问
这个问题问得很实在!我当初啃Kingfisher这段源码的时候也纠结过,咱们一步步拆解下背后的设计逻辑:
核心目的:确保任务管理的线程安全与操作原子性
ImageDownloader内部会维护一个共享的数据结构(比如字典)来跟踪所有ImageFetchLoad任务——比如记录每个URL对应的下载任务实例,避免重复发起下载、统一管理任务生命周期。对这个共享结构的所有读写操作都必须互斥,否则多个线程同时修改会引发数据竞争、崩溃或者状态不一致的问题。
而barrierQueue作为一个带.concurrent属性的队列,配合barrier sync的组合,刚好能满足这个需求:
- 每个通过
barrier sync提交的任务都会成为队列的「屏障任务」:它会等待队列中所有已提交的前置任务执行完毕,然后独占整个队列执行自身逻辑,执行完成后才允许队列处理后续任务。 - 因为所有任务都用
barrier sync提交,相当于让这些关键操作串行化执行,从根本上避免了多线程操作共享资源的冲突。
为什么用sync而不是async?
这里的sync(同步执行)是为了满足「调用者需要立即获取操作结果」的场景:
比如你调用查询方法“这个URL有没有正在下载的任务?”,调用线程必须等待查询操作完成,才能拿到准确、最新的状态。如果用async异步提交,调用线程会直接往下走,无法保证获取到的是最新的任务状态。
为什么不直接用串行队列?
可能有人会疑惑:直接用串行队列不也能保证串行执行吗?搞并发队列+barrier是不是多此一举?
其实这是为了扩展性考虑:
如果未来ImageDownloader需要加入一些不需要互斥的轻量操作(比如不修改共享状态的统计、日志打点),直接把这些任务用普通async提交到这个并发队列即可,它们可以和其他非屏障任务并行执行,而屏障任务依然能保证关键读写操作的原子性。现在所有操作都是屏障任务,只是因为当前所有任务都涉及共享状态的修改或查询,必须互斥。
举个实际场景例子
假设你同时触发两个操作:
addFetchTask(for: someURL):往任务字典里添加新的下载任务cancelFetchTask(for: someURL):从任务字典中移除并取消对应任务
如果不用barrier sync,这两个操作可能同时修改字典,导致遍历字典时崩溃、或者任务状态混乱。而用barrierQueue.sync(flags: .barrier)提交后:
- 两个任务会严格串行执行,先完成添加/取消操作,再执行另一个,保证字典操作的原子性
- 调用线程会等待任务执行完毕,所以添加任务后立即查询状态,能拿到准确的结果
内容的提问来源于stack exchange,提问作者kukushi

