堆栈溢出上的大多数答案都意味着同步与异步行为与串行与并发队列概念差异非常相似。喜欢@Roope的第一条评论中的链接
我开始认为 串行和并发与DispatchQueue
有关,同步/异步表示操作如何在线程上执行。 我说的对吗?
就像我们有DQ.main.sync
,那么任务/操作闭包将在这个串行(主)队列上以同步方式执行。 而且,如果我这样做DQ.main.async
那么任务将在其他一些后台队列上异步获取,并且在完成时将返回主线程上的控制权。 而且,由于 main 是一个串行队列,因此在当前闭包任务完成执行之前,它不会让任何其他任务/操作进入执行状态/开始执行。
然后DQ.global().sync
将在分配了其任务/操作的线程上同步执行任务,即,它将通过阻止该特定线程上的任何上下文切换来阻止该线程执行任何其他任务/操作。 而且,由于全局是一个并发队列,因此无论先前任务/操作的执行状态如何,它都会继续将其中存在的任务置于执行状态。
DQ.global().async
将允许在已执行操作闭包的线程上进行上下文切换
这是对上述调度队列和同步与异步的正确解释吗?
你问了正确的问题,但我认为你有点困惑(主要是由于互联网上关于这个主题的帖子不是很清楚)。
并发/串行
让我们看看如何创建新的调度队列:
let serialQueue = DispatchQueue(label: label)
如果未指定任何其他附加参数,则此队列将表现为串行队列: 这意味着在这个队列上调度的每个块(同步或异步无关紧要)都将单独执行,而不可能在同一队列上同时执行其他块。
这并不意味着其他任何东西都被停止了,它只是意味着如果在同一队列上调度了其他东西,它将等待第一个块完成,然后再开始执行。其他线程和队列仍将自行运行。
但是,您可以创建一个并发队列,该队列不会以这种方式约束此代码块,相反,如果碰巧在同一队列上同时调度更多代码块,它将同时执行它们(在不同的线程上)。
let concurrentQueue = DispatchQueue(label: label,
qos: .background,
attributes: .concurrent,
autoreleaseFrequency: .inherit,
target: .global())
因此,您只需要将属性concurrent
传递给队列,它就不再是串行的。
(我不会谈论其他参数,因为它们不是这个特定问题的焦点,我认为,您可以在评论中链接的其他 SO 帖子中阅读它们,或者,如果还不够,您可以问另一个问题)
如果您想了解有关并发队列的更多信息(又名:如果您不关心并发队列,请跳过)
您可能会问:我什么时候甚至需要并发队列?
好吧,举个例子,让我们考虑一个用例,您希望在共享资源上同步 READS:由于读取可以同时完成而不会出现问题,因此您可以使用并发队列。
但是,如果您想在该共享资源上写入怎么办? 在这种情况下,写入需要充当"屏障",并且在执行该写入期间,没有其他写入和任何读取可以同时对该资源进行操作。 为了获得这种行为,swift 代码将如下所示
concurrentQueue.async(flags: .barrier, execute: { /*your barriered block*/ })
因此,换句话说,您可以在需要时使并发队列暂时用作串行队列。
再一次,并发/串行区别仅适用于调度到同一队列的块,它与可以在另一个线程/队列上完成的其他并发或串行工作无关。
同步/异步
这完全是另一个问题,与前一个问题几乎没有联系。
这两种调度某些代码块的方法相对于调度调用时的当前线程/队列。此调度调用在执行您在另一个队列上调度的代码时会阻止(在同步的情况下)或不阻止(异步)该线程/队列的执行。
因此,假设我正在执行一个方法,并且在该方法中,我在其他队列上调度异步内容(我正在使用主队列,但它可能是任何队列):
func someMethod() {
var aString = "1"
DispatchQueue.main.async {
aString = "2"
}
print(aString)
}
发生的情况是,此代码块被调度到另一个队列上,并且可以在该队列上串行或并发执行,但这与当前队列(调用 someMethod 的队列)上发生的情况无关。
当前队列上发生的情况是,代码将继续执行,并且不会等待该块完成,然后再打印该变量。 这意味着,您很可能会看到它打印 1 而不是 2。(更准确地说,你无法知道首先会发生什么)
相反,如果您调度它同步,那么您将始终打印 2 而不是 1,因为当前队列会等待该代码块完成,然后再继续执行。
所以这将打印 2:
func someMethod() {
var aString = "1"
DispatchQueue.main.sync {
aString = "2"
}
print(aString)
}
但这是否意味着调用 someMethod 的队列实际上已停止?
好吧,这取决于当前队列:
- 如果它是串行的,那么是的。之前调度到该队列或将在该队列上调度的所有块都必须等待该块完成。
- 如果是并发的,则没有。所有并发块将继续执行,只有这个特定的执行块将被阻止,等待此调度调用完成其工作。当然,如果我们处于障碍的情况下,那么就像串行队列一样。
当当前队列和我们调度的队列相同时会发生什么?
假设我们在串行队列上(我认为这将是您的大多数用例)
- 万一我们调度同步,比死锁。该队列上将不再执行任何内容。这是可能发生的最坏情况。
- 如果我们异步调度,那么代码将在该队列上已经调度的所有代码的末尾执行(包括但不限于现在在 someMethod 中执行的代码)
因此,在使用同步方法时要格外小心,并确保你不在调度到的同一个队列中。
我希望这能让你更好地理解。
我开始认为串行和并发与调度队列有关,并且同步/异步与如何在线程上执行操作有关。
总之:
目标队列是串行的还是并发的决定了该目标队列的行为方式(即,该队列是否可以与调度到同一队列的其他事物同时运行此闭包);
而
sync
vsasync
规定了您从中调度的当前线程的行为方式(即,调用线程是否应该等到调度的代码完成)。
因此,串行/并发会影响要调度到的目标队列,而sync
/async
会影响要从中调度的当前线程。
你接着说:
就像我们有
DQ.main.sync
那么任务/操作闭包将在这个串行(主)队列上以同步方式执行。
我可能会将其改写为"如果我们有DQ.main.sync
那么当前线程将等待主队列执行此关闭。
FWIW,我们不经常使用DQ.main.sync
,因为 10 次中有 9 次,我们这样做只是为了调度一些 UI 更新,通常不需要等待。这是次要的,但我们几乎总是使用DQ.main.async
.当我们尝试提供与某些资源的线程安全交互时,我们确实使用sync
。在这种情况下,sync
可能非常有用。但它通常不需要与main
结合使用,而只会引入低效率。
而且,如果我这样做
DQ.main.async
那么任务将在其他一些后台队列上异步获取,并且在完成时将返回主线程上的控制权。
不。
当您执行DQ.main.async
时,您指定闭包将在主队列(您调度到的队列)上异步运行,并且当前线程(可能是后台线程)不需要等待它,但会立即继续。
例如,考虑一个示例网络请求,其响应在URLSession
的后台串行队列上处理:
let task = URLSession.shared.dataTask(with: url) { data, _, error in
// parse the response
DispatchQueue.main.async {
// update the UI
}
// do something else
}
task.resume()
因此,解析发生在这个URLSession
后台线程上,它将 UI 更新调度到主线程,然后继续在此后台线程上执行其他操作。sync
vsasync
的全部目的是"执行其他操作"是否必须等待"更新 UI"完成。在这种情况下,在主线程处理 UI 更新时阻止当前后台线程是没有意义的,因此我们使用async
.
然后,
DQ.global().sync
将在分配了其任务/操作的线程上同步执行任务,即......
是的,DQ.global().sync
说"在后台队列上运行此关闭,但阻止当前线程,直到关闭完成。
不用说,在实践中,我们永远不会做DQ.global().sync
.阻止当前线程等待某些内容在全局队列上运行是没有意义的。将闭包调度到全局队列的全部意义在于您不会阻塞当前线程。如果您正在考虑DQ.global().sync
,您不妨在当前线程上运行它,因为无论如何您都会阻止它。(事实上,GCD 知道DQ.global().sync
不会实现任何目标,并且作为优化,通常会在当前线程上运行它。
现在,如果您出于某种原因要使用async
或使用某些自定义队列,那么这可能是有意义的。但通常做DQ.global().sync
是没有意义的。
。它将通过阻止该特定线程上的任何上下文切换来阻止该线程执行任何其他任务/操作。
不。
sync
不会影响"该线程"(全局队列的工作线程)。sync
会影响从中调度此代码块的当前线程。这个当前线程会等待全局队列执行调度的代码(sync
)还是不执行(async
)?
而且,由于
global
是一个并发队列,因此无论先前任务/操作的执行状态如何,它都会继续将其中存在的任务置于执行状态。
是的。同样,我可以改写一下:"而且,由于global
是当前队列,因此此关闭将安排为立即运行,而不管此队列上可能已经运行了什么。
技术上的区别在于,当您将某些内容调度到并发队列时,虽然它通常会立即启动,但有时不会。也许 CPU 上的所有内核都与运行其他东西有关。或者,也许您已经调度了许多块,并且暂时耗尽了GCD数量非常有限的"工作线程"。最重要的是,虽然它通常会立即启动,但总会有资源限制阻止它这样做。
但这是一个细节:从概念上讲,当您调度到全局队列时,是的,它通常会立即开始运行,即使您可能已经调度到该队列的其他一些尚未完成的闭包。
DQ.global().async
将允许在已执行操作闭包的线程上进行上下文切换。
我可能会避免使用"上下文切换"一词,因为它具有非常具体的含义,可能超出了本问题的范围。如果您真的感兴趣,可以观看 WWDC 2017 视频现代化中央调度使用。
我描述DQ.global().async
的方式是,它只是"允许当前线程继续,解除阻塞,而全局队列执行调度闭包"。这是一种非常常见的技术,通常从主队列调用,以将一些计算密集型代码调度到某个全局队列,但不等待它完成,让主线程可以自由地处理 UI 事件,从而产生响应更快的用户界面。