首先,重要的是要了解线程和队列之间的区别以及 GCD 的真正作用。当我们使用调度队列(通过 GCD)时,我们实际上是在排队,而不是线程。Dispatch 框架是专门为让我们远离线程而设计的,因为 Apple 承认“实施正确的线程解决方案 [可能] 变得极其困难,如果不是 [有时] 不可能实现的话。” 因此,要同时执行任务(我们不想冻结 UI 的任务),我们需要做的就是创建这些任务的队列并将其交给 GCD。GCD 处理所有相关的线程。因此,我们真正要做的就是排队。
马上要知道的第二件事是什么是任务。任务是该队列块中的所有代码(不是在队列中,因为我们可以一直将事物添加到队列中,而是在我们将其添加到队列的闭包中)。任务有时被称为块,块有时被称为任务(但它们通常被称为任务,特别是在 Swift 社区中)。而且无论代码有多少,花括号内的所有代码都被视为一个任务:
serialQueue.async {
// this is one task
// it can be any number of lines with any number of methods
}
serialQueue.async {
// this is another task added to the same queue
// this queue now has two tasks
}
很明显,并发只是意味着与其他事物同时发生,而串行意味着一个接一个(从不同时)。序列化某事,或将某事串行化,只是意味着按照从左到右、从上到下、不间断的顺序从头到尾执行它。
队列有两种类型,串行和并发,但所有队列相对于彼此都是并发的。您想要“在后台”运行任何代码这一事实意味着您想要与另一个线程(通常是主线程)同时运行它。因此,所有调度队列,无论是串行的还是并发的,都相对于其他队列并发地执行它们的任务。任何由队列(串行队列)执行的序列化,只与单个 [串行] 调度队列中的任务有关(如上面的示例中,同一个串行队列中有两个任务;这些任务将在一个之后执行另一个,从不同时)。
串行队列(通常称为私有调度队列)保证任务从开始到结束按添加到特定队列的顺序一次一个地执行。这是讨论调度队列中任何地方的唯一保证序列化--特定串行队列中的特定任务是串行执行的。但是,如果串行队列是单独的队列,则串行队列可以与其他串行队列同时运行,因为所有队列相对于彼此都是并发的。所有任务都在不同的线程上运行,但并非每个任务都保证在同一个线程上运行(不重要,但很有趣)。而且iOS框架没有自带任何现成的串行队列,你必须自己制作。私有(非全局)队列默认是串行的,所以要创建一个串行队列:
let serialQueue = DispatchQueue(label: "serial")
您可以通过其属性属性使其并发:
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: [.concurrent])
但是此时,如果您不向私有队列添加任何其他属性,Apple 建议您只使用其中一个随时可用的全局队列(它们都是并发的)。在这个答案的底部,您将看到另一种创建串行队列的方法(使用目标属性),这是 Apple 推荐的方式(为了更有效的资源管理)。但就目前而言,标记它就足够了。
CONCURRENT QUEUES(通常称为全局调度队列)可以同时执行任务;然而,任务保证按照它们被添加到特定队列的顺序启动,但与串行队列不同,队列不会在启动第二个任务之前等待第一个任务完成。任务(与串行队列一样)在不同的线程上运行,并且(与串行队列一样)并非每个任务都保证在同一个线程上运行(不重要,但很有趣)。iOS 框架附带了四个即用型并发队列。您可以使用上面的示例或使用 Apple 的全局队列之一(通常推荐)创建并发队列:
let concurrentQueue = DispatchQueue.global(qos: .default)
RETAIN-CYCLE RESISTANT:调度队列是引用计数对象,但您不需要保留和释放全局队列,因为它们是全局的,因此忽略保留和释放。您可以直接访问全局队列,而无需将它们分配给属性。
调度队列有两种方式:同步和异步。
SYNC DISPATCHING是指派发队列的线程(调用线程)在派发队列后暂停,并等待该队列块中的任务完成执行后再恢复。同步调度:
DispatchQueue.global(qos: .default).sync {
// task goes in here
}
ASYNC DISPATCHING意味着调用线程在调度队列后继续运行,并且不等待该队列块中的任务完成执行。异步调度:
DispatchQueue.global(qos: .default).async {
// task goes in here
}
现在有人可能会认为,为了串行执行任务,应该使用串行队列,这并不完全正确。为了串行执行多个任务,应该使用串行队列,但所有任务(自身隔离)都是串行执行的。考虑这个例子:
whichQueueShouldIUse.syncOrAsync {
for i in 1...10 {
print(i)
}
for i in 1...10 {
print(i + 100)
}
for i in 1...10 {
print(i + 1000)
}
}
无论您如何配置(串行或并发)或调度(同步或异步)此队列,此任务将始终串行执行。第三个循环永远不会在第二个循环之前运行,第二个循环永远不会在第一个循环之前运行。在使用任何调度的任何队列中都是如此。当您引入多个任务和/或队列时,串行和并发才真正发挥作用。
考虑这两个队列,一个是串行的,一个是并发的:
let serialQueue = DispatchQueue(label: "serial")
let concurrentQueue = DispatchQueue.global(qos: .default)
假设我们以异步方式调度两个并发队列:
concurrentQueue.async {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
101
2
102
103
3
104
4
105
5
他们的输出是混乱的(如预期的那样),但请注意每个队列都在串行执行自己的任务。这是最基本的并发示例——两个任务在后台同时运行在同一个队列中。现在让我们制作第一个序列:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
101
1
2
102
3
103
4
104
5
105
第一个队列不应该串行执行吗?它是(第二个也是)。后台发生的任何其他事情都与队列无关。我们告诉串行队列串行执行,它确实做到了……但我们只给了它一个任务。现在让我们给它两个任务:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
2
3
4
5
101
102
103
104
105
这是序列化的最基本(也是唯一可能的)示例——两个任务在同一队列的后台(到主线程)中串行(一个接一个)运行。但是如果我们让它们成为两个独立的串行队列(因为在上面的例子中它们是同一个队列),它们的输出就会再次混乱:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue2.async {
for i in 1...5 {
print(i + 100)
}
}
1
101
2
102
3
103
4
104
5
105
这就是我说所有队列相对于彼此并发时的意思。这是两个同时执行任务的串行队列(因为它们是独立的队列)。一个队列不知道也不关心其他队列。现在让我们回到两个串行队列(同一个队列)并添加第三个队列,一个并发队列:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue.async {
for i in 1...5 {
print(i + 100)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 1000)
}
}
1
2
3
4
5
101
102
103
104
105
1001
1002
1003
1004
1005
这有点出乎意料,为什么并发队列在执行之前要等待串行队列完成?那不是并发。您的 Playground 可能会显示不同的输出,但我的 Playground 显示了这一点。它显示了这一点,因为我的并发队列的优先级不足以让 GCD 更快地执行它的任务。因此,如果我保持一切不变,但更改全局队列的 QoS(它的服务质量,即队列的优先级)let concurrentQueue = DispatchQueue.global(qos: .userInteractive)
,那么输出与预期的一样:
1
1001
1002
1003
2
1004
1005
3
4
5
101
102
103
104
105
两个串行队列以串行方式执行它们的任务(如预期的那样),并发队列更快地执行其任务,因为它被赋予了高优先级(高 QoS,或服务质量)。
两个并发队列,就像我们的第一个打印示例一样,显示混乱的打印输出(如预期的那样)。为了让它们以串行方式整齐地打印,我们必须使它们都成为相同的串行队列(该队列的相同实例,也不仅仅是相同的标签)。然后每个任务相对于另一个任务被串行执行。然而,让它们串行打印的另一种方法是保持它们同时并发但改变它们的调度方法:
concurrentQueue.sync {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
2
3
4
5
101
102
103
104
105
请记住,同步调度仅意味着调用线程等待队列中的任务完成后再继续。显然,这里的警告是调用线程在第一个任务完成之前被冻结,这可能是也可能不是您希望 UI 执行的方式。
正是由于这个原因,我们不能做以下事情:
DispatchQueue.main.sync { ... }
这是我们无法执行的队列和调度方法的唯一可能组合——在主队列上同步调度。那是因为我们要求主队列冻结,直到我们执行花括号内的任务......我们将其分派到主队列,我们只是冻结了。这称为死锁。要在操场上看到它的实际效果:
DispatchQueue.main.sync { // stop the main queue and wait for the following to finish
print("hello world") // this will never execute on the main queue because we just stopped it
}
// deadlock
最后一件事是资源。当我们给一个队列一个任务时,GCD 从它内部管理的池中找到一个可用的队列。就撰写此答案而言,每个 qos 有 64 个可用队列。这可能看起来很多,但它们可以很快被使用,尤其是第三方库,尤其是数据库框架。出于这个原因,Apple 有关于队列管理的建议(在下面的链接中提到);一个是:
将任务提交到全局并发调度队列之一,而不是创建私有并发队列。对于串行任务,将串行队列的目标设置为全局并发队列之一。
这样,您可以保持队列的序列化行为,同时最大限度地减少创建线程的单独队列的数量。
为此,Apple 建议不要像以前那样创建它们(您仍然可以),而是像这样创建串行队列:
let serialQueue = DispatchQueue(label: "serialQueue", qos: .default, attributes: [], autoreleaseFrequency: .inherit, target: .global(qos: .default))
使用扩展,我们可以把它归结为:
extension DispatchQueue {
public class func serial(label: String, qos: DispatchQoS = .default) -> DispatchQueue {
return DispatchQueue(label: label,
qos: qos,
attributes: [],
autoreleaseFrequency: .inherit,
target: .global(qos: qos.qosClass))
}
}
let defaultSerialQueue = DispatchQueue.serial(label: "xyz")
let serialQueue = DispatchQueue.serial(label: "xyz", qos: .userInteractive)
// Which now looks like the global initializer
let concurrentQueue = DispatchQueue.global(qos: .default)
为了进一步阅读,我推荐以下内容:
https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008091-CH1-SW1
https://developer.apple.com/documentation/dispatch/dispatchqueue