在 GCD 中,串行队列通过异步操作 Swift 同步
Posted
技术标签:
【中文标题】在 GCD 中,串行队列通过异步操作 Swift 同步【英文标题】:In GCD are serial queue synchronous by asynchronous operation Swift 【发布时间】:2018-03-13 06:31:07 【问题描述】:我正在使用具有 QoS 背景的串行队列
let serialQueue = DispatchQueue(label: "queue1", qos: DispatchQoS.background)
分配一个同步作业和两个异步作业:
func serialTask()
serialQueue.sync
for i in 0..<10
print("????", i)
serialQueue.async
for i in 20..<30
print("⚪️", i)
serialQueue.async
for i in 101..<120
print("????", i)
所有 3 个作业都在一个接一个地执行同步,但最后两个作业是异步的。异步作业是否在串行队列中同步。
【问题讨论】:
【参考方案1】:让我看看我能否澄清async
与sync
之间的区别。
我将在我的示例中使用一些更改:
我将使用 Instruments 的“兴趣点”来显示任务何时运行,而不是使用 print
语句。 (请参阅 WWDC 2019 Getting Started With Instruments。)这样我们可以以图形方式查看行为。
我将在分派某些东西时发布一个简单的“兴趣点”事件路标 (Ⓢ),并将分派的任务包装在一个“兴趣区域”(水平条)中,以图形方式说明某个过程的持续时间。
我会将您的 for
循环更改为 Thread.sleep(forTimeInterval: 1)
,模拟一些耗时的过程。如果您只是有一个快速的for
循环,事情会发生得如此之快,以至于无法辨别线程的实际情况。
所以,考虑一下:
import os.signpost
private let pointsOfInterest = OSLog(subsystem: "GCD Demo", category: .pointsOfInterest)
func tasks(on queue: DispatchQueue)
pointsOfInterestRange(with: "tasks(on:)")
os_signpost(.event, log: pointsOfInterest, name: "1") // first Ⓢ
queue.sync self.oneSecondProcess(with: "1")
os_signpost(.event, log: pointsOfInterest, name: "2") // second Ⓢ
queue.async self.oneSecondProcess(with: "2")
os_signpost(.event, log: pointsOfInterest, name: "3") // third Ⓢ
queue.async self.oneSecondProcess(with: "3")
func oneSecondProcess(with staticString: StaticString)
pointsOfInterestRange(with: staticString)
Thread.sleep(forTimeInterval: 1)
func pointsOfInterestRange(with staticString: StaticString, block: () -> Void)
let identifier = OSSignpostID(log: pointsOfInterest)
os_signpost(.begin, log: pointsOfInterest, name: staticString, signpostID: identifier)
block()
os_signpost(.end, log: pointsOfInterest, name: staticString, signpostID: identifier)
这就像您的示例,但不是print
声明,我们有路标声明,在 Instruments 的“兴趣点”工具中生成以下图形时间线:
所以,你可以看到:
底部的tasks(on:)
函数发出sync
调度,第一个Ⓢ路标。
它等待sync
任务“1”在继续之前完成,此时它发出两个后续调度,第二个和第三个Ⓢ路标(它们连续发生得如此之快以至于它们在图中重叠)。
但tasks(on:)
不会等待两个async
任务“2”和“3”完成。一旦它完成了那些async
任务的调度,它就会立即返回(因此tasks(on:)
范围会立即停止)。
因为后台队列是串行的,所以三个分派的任务(“1”、“2”和“3”)一个接一个地依次运行。
但是,如果您将其更改为使用并发队列:
let queue = DispatchQueue(label: "...", attributes: .concurrent)
然后您可以看到两个async
任务现在相对于彼此同时运行:
这一次,task(on:)
调度 sync
调用,等待它完成,然后,只有当 sync
调用完成后,seriesOfTasks
才能继续调度两个 async
调用(在此情况下,不等待那些分派的任务完成)。
如您所见,async
和 sync
的行为是不同的。使用sync
,调用线程将等待分派的任务完成,但使用async
,它不会。
从上面可以得出两个主要结论:
sync
与 async
的选择决定了当前线程的行为(即是否等待分派的任务)。
而且,作为一般规则,我们通常会避免在做任何耗时的事情时从主线程调用sync
(因为这最终会阻塞主线程)。
串行队列还是并发队列的选择决定了您分派的工作的行为,即它是否可以相对于该队列上的其他任务同时运行,或者它们是否会一个接一个地连续运行。
【讨论】:
【参考方案2】:同步和异步执行与底层队列无关。同步执行意味着调用线程必须等到块完成。因此,第二个块在第一个块完成后入队。异步意味着调用者不得等待块的完成。因此,第三个块在前面的serialQueue.async
语句之后直接入队,而第二个块仍在运行甚至等待执行。
在你的函数serialTask()
结束时,保证第一个块被执行。第二个和第三个块入队,但不确定它们是否已执行、正在运行甚至等待执行。由于您使用的是串行队列,因此可以确定第二个块在第三个块之前执行。
您可以通过添加来检查最后两个块的异步执行
serialQueue.async
print "sleep"
sleep(10);
print "awake"
在async
的两次调用之前,您将观察到以下内容:
-
第一个块将立即执行,
sleep
将立即打印。
serialTask()
的执行时间大大少于 10 秒。
awake
仅在 10 秒后输出(惊喜,惊喜)。
第二个和第三个块也在 10 秒后执行,在 serialTask()
结束之后很长时间。
这两个块的延迟执行意味着异步。如果您将第一个(同步)块移动到 serialTask()
的末尾,则会发生以下情况:
sleep
将立即打印出来。
serialTask()
的执行大约需要 10 秒。在执行完最后的同步块后结束。
awake
仅在 10 秒后输出(惊喜,惊喜)。
在打印出awake
之后,也会执行第二个和第三个块。
【讨论】:
好的,但是最后两个异步任务没有执行异步,行为与同步相同。 @salmansiddiqui:这两个块是异步执行的。你从你不幸的例子中推断出它是普遍有效的。这会导致你得出错误的结论。请查看我的编辑。 这里你已经添加了睡眠,但行为是一样的,它会等待一段时间并执行。 @salmansiddiqui 这个答案非常准确,clemens 提供了证明它的工具。我说了很多同样的话,但效果要差得多,也没有证据。我赞成这个。 @Smartcat 如何将延迟执行视为异步【参考方案3】:所以同步任务立即运行,然后当它完成时,您将两个异步任务添加到该队列,*然后您的方法返回。 *然后,这两个任务将按顺序处理,只要该队列空闲。
因此,如果在调用此方法之前该队列中有任何待处理的异步任务,这些任务将在您的两个异步任务之前运行。
【讨论】:
以上是关于在 GCD 中,串行队列通过异步操作 Swift 同步的主要内容,如果未能解决你的问题,请参考以下文章
IOS多线程知识总结/队列概念/GCD/串行/并行/同步/异步
iOS多线程知识总结/队列概念/GCD/串行/并行/同步/异步