Dispatch ( 全称 Grand Central Dispatch,简称 GCD ) 是一套由 Apple 编写以提供让代码以多核并发的方式执行应用程序的框架。
DispatchQueue
( 调度队列 ) 就是被定义在 Dispatch 框架中,可以用来执行跟多线程有关操作的类。
在使用它之前,我们得先了解一下基本概念,我会先简单介绍,后面再根据讲解的内容逐步详细介绍,目的是为了方便读者融入。
PS:如果在阅读时发现有任意错误,请指点我,感谢!
同步和异步执行
如图。同步和异步的区别在于,线程会等待同步任务执行完成;线程不会等待异步任务执行完成,就会继续执行其他任务/操作。
阅读指南:
本文中出现的 "任务" 是指
sync {}
和async {}
中整个代码块的统称,"操作" 则是在 "任务" 中执行的每一条指令 ( 代码 ) ;因为主线程没有 "任务" 之说,主线程上执行的每一条 ( 段 ) 代码,都统称为 "操作"。
串行和并发队列
在 GCD 中,任务由**队列 (串行或并发) **负责管理和决定其执行顺序,在一条由系统自动分配的线程上执行。
在串行 (Serial) 队列中执行任务时,任务会按照固定顺序执行,执行完一个任务后再继续执行下一个任务 (这意味着串行队列同时只能执行一个任务) ;在并发 (Concurrent) 队列中执行任务时,任务可以同时执行 ( 其实是在以极短的时间内不断的切换线程执行任务 ) 。
串行和并发队列都以 先进先出 (FIFO) 的顺序执行任务,任务的执行流程如图:
示例1 - 在串行队列中执行同步 ( sync
) 任务
// 创建一个队列(默认就是串行队列,不需要额外指定参数)
let queue = DispatchQueue(label: "Serial.Queue")
print("thread: \(Thread.current)")
queue.sync {
(0..<5).forEach { print("rool-1 -> \($0): \(Thread.current)") }
}
queue.sync {
(0..<5).forEach { print("rool-1 -> \($0): \(Thread.current)") }
}
/**
thread: <NSThread: 0x281951f40>{number = 1, name = main}
rool-1 -> 0: <NSThread: 0x281951f40>{number = 1, name = main}
rool-1 -> 1: <NSThread: 0x281951f40>{number = 1, name = main}
rool-1 -> 2: <NSThread: 0x281951f40>{number = 1, name = main}
rool-1 -> 3: <NSThread: 0x281951f40>{number = 1, name = main}
rool-1 -> 4: <NSThread: 0x281951f40>{number = 1, name = main}
rool-2 -> 0: <NSThread: 0x281951f40>{number = 1, name = main}
rool-2 -> 1: <NSThread: 0x281951f40>{number = 1, name = main}
rool-2 -> 2: <NSThread: 0x281951f40>{number = 1, name = main}
rool-2 -> 3: <NSThread: 0x281951f40>{number = 1, name = main}
rool-2 -> 4: <NSThread: 0x281951f40>{number = 1, name = main}
*/
复制代码
没什么好解释的,结果肯定是按照正常的顺序来,一个接着一个地执行。因为同步执行就是会一直等待,等到一个任务全部执行完成后,再继续执行下一个任务。
有一点需要注意的是,主线程和在同步任务中 Thread,current
的打印结果相同,也就是说,队列中的同步任务在执行时,系统给它们分配的线程是主线程,因为同步任务会让线程等待它执行完,既然会等待,那就没有再开辟线程的必要了。
关于主线程和主队列
当应用程序启动时,就有一条线程被系统创建,与此同时这条线程也会立刻运行,该线程通常叫做程序的主线程。
同时系统也为我们提供一个名为主队列 ( DispatchQueue.main {}
) 的串行特殊队列,默认我们写的代码都处于主队列中,主队列中的所有任务都在主线程执行。
示例2 - 在串行队列中执行异步 ( async
) 任务
let queue = DispatchQueue(label: "serial.com")
print("thread: \(Thread.current)")
(0..<50).forEach {
print("main - \($0)")
// 让线程休眠0.2s,目的是为了模拟耗时操作,不再赘述。
Thread.sleep(forTimeInterval: 0.2)
}
queue.async {
(0..<5).forEach {
print("rool-1 -> \($0): \(Thread.current)")
Thread.sleep(forTimeInterval: 0.2)
}
}
queue.async {
(0..<5).forEach {
print("rool-2 -> \($0): \(Thread.current)")
Thread.sleep(forTimeInterval: 0.2)
}
}
/**
thread: <NSThread: 0x281251fc0>{number = 1, name = main}
main - 0
main - 1
main - 2 ... 顺序执行到 49
rool-1 -> 0: <NSThread: 0x281234100>{number = 3, name = (null)}
rool-1 -> 1: <NSThread: 0x281234100>{number = 3, name = (null)}
rool-1 -> 2: <NSThread: 0x281234100>{number = 3, name = (null)}
rool-1 -> 3: <NSThread: 0x281234100>{number = 3, name = (null)}
rool-1 -> 4: <NSThread: 0x281234100>{number = 3, name = (null)}
rool-2 -> 0: <NSThread: 0x281234100>{number = 3, name = (null)}
rool-2 -> 1: <NSThread: 0x281234100>{number = 3, name = (null)}
rool-2 -> 2: <NSThread: 0x281234100>{number = 3, name = (null)}
rool-2 -> 3: <NSThread: 0x281234100>{number = 3, name = (null)}
rool-2 -> 4: <NSThread: 0x281234100>{number = 3, name = (null)}
*/
复制代码
可以看到,线程一定会等待它当前的操作 ( 包括让线程休眠 ) 执行完后,再继续执行 async
任务。此时任务同样按顺序执行,因为串行队列只能执行完一个任务后再继续执行下一个任务。
任务中 Thread.current
的打印结果都是 number = 3
,换句话说,串行队列中的异步任务在执行时,系统给它们开辟的线程是其他线程,并且只开辟一个,因为串行队列同时只能执行一个任务,因此没有开启多条线程的必要。
关于让线程休眠
这里解释一下 Thread.sleep
这个方法的作用:是让当前线程暂停任何操作0.2s。
请注意我说的是当前线程,不要误以为是让整个应用程序都停止了,不是这样的。如果当前任务所在的线程停止了,是不会影响到别的线程正在执行任务的,这点要区分清楚。
PS:也就是说,在上面同步任务中,为了测试而调用的 Thread.sleep
方法并没有作用 ( 但是为了测试和验证,依然调用了 ) ,因为任务都在一条线程上,并按照固定顺序执行。
示例3 - 在串行队列中执行异步 ( async
) 任务 II
let queue = DispatchQueue(label: "serial.com")
print("1: \(Thread.current)")
queue.async { print("2: \(Thread.current)") }
print("3: \(Thread.current)")
queue.async { print("4: \(Thread.current)") }
print("5: \(Thread.current)")
/**
1: <NSThread: 0x28347ed00>{number = 1, name = main}
3: <NSThread: 0x28347ed00>{number = 1, name = main}
2: <NSThread: 0x2834268c0>{number = 3, name = (null)}
5: <NSThread: 0x28347ed00>{number = 1, name = main}
4: <NSThread: 0x2834268c0>{number = 3, name = (null)}
*/
复制代码
这时候打印的顺序并不固定,但肯定会先从 1
开始打印,打印的结果可能是:12345, 12354, 13254, 13245, 13524, 13254...
,这是为什么?我们先来了解一些概念后再来回顾。
队列和任务的关系
首先要解释一下同步和异步这两个词的概念,既然是同步或异步,也能解释为相同,或是不同,它需要一个作为参照的对象,来知道它们相对于这个对象来说到底是相同,还是不同。
那在 GCD 中,它们的参照对象就是我们的主线程 ( dispatchQueue.main
) 。也就是说如果是同步任务,那就在主线程执行;而如果是异步任务,那就在其他线程执行。
这就解释了,为什么串行队列在执行异步任务时,还会开启线程,所谓异步嘛,那就是不在主线程执行,区别是串行队列只会开启一条线程,而并发队列会开启多条线程。
而同步任务是,甭管它是什么队列和任务,只要执行的是同步任务,就在主线程执行 。
-
异步任务
异步任务说:“我要开始执行任务了,快给我分配线程让我执行。”
应用程序说:“好!我另外开辟线程出来让你执行,等等,请问你所处的队列是?”
异步任务说:“串行队列。”
应用程序说:“既然是串行队列,而串行队列中的所有任务都会按照固定顺序执行,只能执行完一个任务后再继续执行下一个任务 ( 这意味着串行队列同时只能执行一个任务 ) ,那我就只给你分配一条线程吧!你队列中的所有任务、包括你,都在这条线程上顺序执行。”
异步任务说:“那如果我处在并发队列中呢?”
应用程序说:“如果是在并发队列中,那队列中的所有任务可以同时执行,我会给你分配多条线程,让每个任务可以在不同的线程上同时执行。”
-
同步任务
同步任务说:“我要开始执行任务了,快给我分配线程让我执行。”
应用程序说:“既然是同步任务那就相当于在主线程执行,那我就给你主线程来执行吧!”
同步任务说:“我的待遇太差了。”
任务和线程的关系
任务只有两种,同步任务和异步任务,无论同步任务是处在什么队列中,它都会让当前正在执行的线程等待它执行完成,例如:
// 当前线程执行打印 main-1 的操作
print("main-1")
// 线程执行到这里发现遇到一个 sync 任务,就会在此等待,
// 直到 sync 任务执行完成,才会继续执行其他操作。
//
// 串行或并发队列
queue.sync {
(0..<10).forEach {
print("sync \($0): \(Thread.current)")
Thread.sleep(forTimeInterval: 0.5)
}
}
// 等待!线程等待 sync 执行完后,再继续执行打印 main-2 的操作。
print("main-2")
/**
main-1
sync 0: <NSThread: 0x6000011968c0>{number = 1, name = main}
sync 1: <NSThread: 0x6000011968c0>{number = 1, name = main}
sync 2: <NSThread: 0x6000011968c0>{number = 1, name = main}
sync 2 ...9
main-2
*/
复制代码
而如果是异步任务,不管它处在什么队列中,当前线程都不会等待它执行完成,例如:
// 当前线程执行打印 main-1 的操作
print("main-1")
// 线程执行到这里发现遇到一个 async 任务,
// 那么线程不会等待它执行完成,就会继续执行其他操作。
//
// 串行或并发队列
queue.async {
(0..<20).forEach { print("async \($0)") }
}
// 开辟线程的时间大约是90微妙,加上循环的准备以及打印时间,
// 这里给它200微妙,测试async任务中的线程和当前线程之间的执行顺序。
Thread.sleep(forTimeInterval: 0.0002000)
// 不会等待!线程不会等待 async 执行完成就会执行打印 main-2 的操作
print("main-2")
复制代码
打印的结果可能稍有不同,但是肯定先从 main-1
开始打印。虽然 main-2
是执行在 async
后面的,async
也会先执行,但是由于当前线程不等待它执行完成的机制,所以它在执行到某一刻时如果到了线程需要打印 main-2
的时间,就会执行打印 main-2
的操作。也有可能是,main-2
先执行,然后等到了某一时刻再执行 async
中的任务 ( 开辟线程需要时间 ) 。
也就是说,这里当前线程和 async
任务中的线程在执行时是不阻塞对方的 ( 互不等待 ) ,本次运行结果如下:
/**
main-1
async 0
async 1
async 2
main-2
async 3
async 4
async 5
...
*/
复制代码
PS:我是怎么知道开辟线程的时间大约是 90 微妙的?因为我看了线程成本中的描述。
回顾
这就能解释之前示例中的执行顺序了,再来回顾一下:
let queue = DispatchQueue(label: "serial.com")
print("1: \(Thread.current)")
queue.async { print("2-\(Thread.current)") }
print("3: \(Thread.current)")
queue.async { print("4: \(Thread.current)") }
print("5: \(Thread.current)")
复制代码
虽然执行顺序不固定,但还是有一定的规律可循的,因为是串行队列,所以在主线程中 1, 3, 5
一定按顺序执行,而在 async
线程中 2, 4
也一定按顺序执行。
示例4 - 串行队列死锁
首先,并发队列不会出现死锁的情况;其次,在串行队列中,只有 sync { sync {} }
和 async { sync {} }
会出现死锁,内部的 sync closure 永远不会被执行,并且程序会崩溃,例如:
queue.sync {
print("1")
queue.sync { print("2") }
print("3")
}
// Prints "1"
queue.async {
print("1")
queue.sync { print("2") }
print("3")
}
// Prints "1"
复制代码
仔细观察上面的代码就会发现,只有内部套用 sync {}
的情况下才会死锁,那使用 sync
( 同步 ) 意味着什么呢?这意味着,当前线程会等待同步任务执行完成。可问题是,这个 sync
任务是嵌套在另一个任务里面的 ( sync { sync {} }
) ,那这里就有两个任务了。
由于串行队列是执行完当前任务后,再继续执行下一个任务。放到这里就是,内部的 sync {}
想要执行的话,它必须要等待外部的 sync {}
执行完成,那外部的 sync {}
能不能执行完成呢?由于这个内部任务是同步的,它会阻塞当前正在执行外部 sync {}
的线程,让当前线程等待它 ( 内部 sync {}
) 执行完成,可问题是外部的 sync {}
完成不了的话,内部的 sync {}
也无法执行,结果就是一直等待,谁都无法继续执行,造成死锁。
既然线程会等待内部的同步任务执行完成,又限制串行队列同时只能执行一个任务,那在外部的 sync {}
没有执行完成之前,内部的 sync {}
永远不能执行,而外部线程在等待内部 sync {}
执行完成的条件下,导致外部的 sync {}
也无法执行完成。
总结:因为串行队列同时只能执行一个任务,就意味着无论如何,线程只能先执行完当前任务后,再继续执行下一个任务。而同步任务的特点是,会让线程等待它执行完成。那问题就来了,我 ( 线程 ) 既不可能先去执行它,又要等待它,结果是导致外部任务永远无法执行完成,而内部的任务也永远无法开启。
对于第二段代码 async { sync {} }
的死锁,原理是一样的,不要被它外部的 async {}
给迷惑了,内部的 sync {}
同样会阻塞它的线程执行,阻塞的结果就是外部的 async {}
无法执行完成,内部的 sync {}
也永远无法开启。
至于串行队列另外两种任务的嵌套结构 sync { async {} }
和 async { async }
,例如:
queue.sync {
print("task-1")
queue.async {
(0..<10).forEach {
print("task-2: \($0) \(Thread.current)")
Thread.sleep(forTimeInterval: 0.5)
}
}
print("task-1 - end")
}
/**
1
task-1 - end
task-2: 0 <NSThread: 0x6000019c0d80>{number = 3, name = (null)}
task-2: 1 <NSThread: 0x6000019c0d80>{number = 3, name = (null)}
task-2: 2 ... 9
*/
queue.sync {
print("task-1")
queue.async {
(0..<10).forEach {
print("task-2: \($0) \(Thread.current)")
Thread.sleep(forTimeInterval: 0.5)
}
}
print("task-1 - end")
}
/**
1
task-1 - end
task-2: 0 <NSThread: 0x6000019c0d80>{number = 3, name = (null)}
task-2: 1 <NSThread: 0x6000019c0d80>{number = 3, name = (null)}
task-2: 2 ... 9
*/
复制代码
虽然已经不再死锁,但执行的顺序稍有不同,可以看到,程序是先把外部任务执行完后,再去执行内部任务。这是因为,内部的 async {}
已经不再阻塞当前线程,又因为串行队列只能先把当前任务执行完后,再去执行下一个任务,那自然而然就是先把外部任务执行完后,再接着去执行内部的 async {}
任务了。
示例5 - DispatchQueue.main 特殊串行主队列
前面说过,async
中的任务都会在其他线程执行,那对于主队列中的 async
呢?在项目中我们经常调用的 DispatchQueue.main.asyncAfter(deadline:)
难道是在其他线程执行吗?其实不是的,如果是 DispatchQueue.main
自己的队列,那么即使是 async
,也会在主线程执行,由于主队列本身是串行队列,也是同时只能执行一个任务,所以是,它会在处理完当前任务后,再去处理 async
中的任务,例如:
// 实际上相当于在 DispatchQueue.main.sync {} 中执行
print("1")
DispatchQueue.main.async {
(0..<10).forEach {
print("async\($0) \(Thread.current)")
Thread.sleep(forTimeInterval: 0.2)
}
}
print("3")
/**
1
3
async0 <NSThread: 0x6000007928c0>{number = 1, name = main}
async1 <NSThread: 0x6000007928c0>{number = 1, name = main}
async2 <NSThread: 0x6000007928c0>{number = 1, name = main}
async3 ...9
*/
复制代码
虽然 async
不阻塞当前线程执行,但是由于都在一个队列上,DispatchQueue.main
只能先执行完当前任务后,再继续执行下一个任务 ( async
) 。
而如果在主线程调用 DispatchQueue.main.sync {}
又会如何呢?答案是:会死锁。其实原因很简单,因为整个主线程的代码就相当于放在一个大的 DispatchQueue.main.sync {}
任务中,这时候如果再调用 DispatchQueue.main.sync {}
,结果肯定是死锁。
还有一点需要留意,一定要在主线程执行和有关 UI 的操作,如果是在其他线程执行,例如:
queue.async { // 并发队列
customView.backgroundColor = UIColor.blue
}
复制代码
很可能就会接收到一个 Main Thread Checker: UI API called on a background thread: -[UIView setBackgroundColor:]
的崩溃报告,因此主线程也被称为 UI 线程。
示例6 - 在并发队列中执行同步 ( sync
) 任务
let queue = DispatchQueue(label: "serial.com", attributes: .concurrent)
queue.sync {
(0..<10).forEach {
print("task-1 \($0): \(Thread.current)")
Thread.sleep(forTimeInterval: 0.2)
}
}
print("main-1")
queue.sync {
(0..<10).forEach {
print("task-2 \($0): \(Thread.current)")
Thread.sleep(forTimeInterval: 0.2)
}
}
print("main-2")
/**
task-1 0: <NSThread: 0x6000023968c0>{number = 1, name = main}
task-1 1: <NSThread: 0x6000023968c0>{number = 1, name = main}
task-1 2: <NSThread: 0x6000023968c0>{number = 1, name = main}
task-1 3: <NSThread: 0x6000023968c0>{number = 1, name = main}
task-1 4: <NSThread: 0x6000023968c0>{number = 1, name = main}
main-1
task-2 0: <NSThread: 0x6000023968c0>{number = 1, name = main}
task-2 1: <NSThread: 0x6000023968c0>{number = 1, name = main}
task-2 2: <NSThread: 0x6000023968c0>{number = 1, name = main}
task-2 3: <NSThread: 0x6000023968c0>{number = 1, name = main}
task-2 4: <NSThread: 0x6000023968c0>{number = 1, name = main}
main-2
*/
复制代码
使用并发队列执行同步任务和在主线程执行操作并没有区别,因为 sync
会牢牢的将当前线程固定住,让线程等待它执行完成后才能继续执行其他操作。这里也能够看到,main-1
和 main-2
分别等待 sync
执行结束后才能执行。
示例7 - 在并发队列中执行异步 ( async
) 任务
在线程将要执行到某个队列的 async
时,队列才会开始并发执行任务,线程不可能跨越当前正在执行的操作去启动任务。举个例子:
// 指定为创建并发队列 (.concurrent)
let queue = DispatchQueue(label: "concurrent.com", attributes: .concurrent)
(0..<100).forEach {
print("main-\($0)")
Thread.sleep(forTimeInterval: 0.02)
}
queue.async { print("task-1", Thread.current) }
queue.async { print("task-2", Thread.current) }
queue.async { print("task-3", Thread.current) }
queue.async { print("task-4", Thread.current) }
queue.async { print("task-5", Thread.current) }
queue.async { print("task-6", Thread.current) }
print("main-end")
/**
main-0
main-1
main-2 ...99
task-2 <NSThread: 0x282e387c0>{number = 3, name = (null)}
task-4 <NSThread: 0x282e387c0>{number = 3, name = (null)}
task-5 <NSThread: 0x282e387c0>{number = 3, name = (null)}
task-3 <NSThread: 0x282e38800>{number = 5, name = (null)}
task-6 <NSThread: 0x282e387c0>{number = 3, name = (null)}
print("main-end")
task-1 <NSThread: 0x282e04b40>{number = 4, name = (null)}
*/
复制代码
因为主线程也是串行队列,程序将按照顺序执行,等到所有循环执行完成后,才会执行 queue.async
,由于是并发队列,所有任务都会同时执行,执行顺序并不固定,而最后的 main-end
可能安插在队列中某个任务完成前后的地方。
因为在执行 main-end
之前,任务已经被队列并发出去了。对于主线程来说,它完成打印 main-end
的时间是固定的,但是队列中并发任务的执行完成的时间并不固定 ( 执行任务会消耗时间 ) 。这时主线程并不会等待 async
的所有任务执行结束就会继续执行打印 main-end
的操作。
所以是,如果在执行 async
的某个时间内刚好到了主线程需要打印 main-end
的时间,就会执行打印 main-end
的操作,而 async
中还没有完成的任务将会继续执行,如图:
可以看到,循环操作结束后,队列才开始并发执行任务,打印 main-end
的操作在 queue.async
之后执行,但是由于队列执行任务需要时间,所以 main-end
有可能在 queue.async
执行完成之前执行。
对于一条线程来说,它的所有操作绝对按照固定顺序执行,不存在一条线程同时执行多个任务的情况。而我们的所谓并发,就是给每个任务开辟一条线程出来执行,等到有某个线程执行完后,就会复用这条线程去执行其他在队列中还没有开始执行的任务。
一条线程只负责执行它当前任务中的所有操作,至于其他线程被开启后 ( 前提是不要开启同样的线程 ) ,它们就在各自的线程上分别独立执行任务,互不影响。举个例子:
假设你要跑100米,当跑到50米的时候,就会有5个人跟你一起跑,跑到终点的时候,可能是你跑得比他们都快,也有可能是他们之中的任意人跑得比你快。
那你就可以想象成那 "5个人" 就是并发中的任务 ( 同时执行) ,而 "你" 就是当前线程。
示例8 - 并发队列的疑惑 - sync { sync {} }
那什么时候会开启同样的线程呢?也就是说,假设有一条线程 3 在执行,那么在这条线程 3 还没有执行完成的时候,就又有一条线程为 3 的任务开启了。这对于 async
任务来说,几乎不可能 ( 我说几乎是因为我不确定,按照我的猜测,应该不会出现这种情况 ) ,也就是说,想要开启同样的一条线程执行异步任务,必须要等到前面的线程执行完后,再用这条线程去执行其他任务。
但是对于 sync
任务来说,在 sync
还没执行完的时候,我可以在 sync {}
内部又开启一个 sync {}
任务,因为 sync {}
注定在主线程执行 ( async
任务无法指定在哪一条线程执行,而是由系统自动分配 ) ,这样一来,就有了在一条线程还没有执行完的时候,就又有一条同样的线程开启执行任务了。在串行队列中,我们已经知道,这样做会造成死锁,那在并发队列中又会如何呢?例如:
let queue = DispatchQueue(label: "concurrent.com", attributes: .concurrent)
queue.sync {
print("sync-start")
queue.sync {
(0..<5).forEach {
print("task \($0): \(Thread.current)")
Thread.sleep(forTimeInterval: 0.5)
}
}
print("sync-end")
}
/**
sync-start
task 0: <NSThread: 0x600003b828c0>{number = 1, name = main}
task 1: <NSThread: 0x600003b828c0>{number = 1, name = main}
task 2: <NSThread: 0x600003b828c0>{number = 1, name = main}
task 3: <NSThread: 0x600003b828c0>{number = 1, name = main}
task 4: <NSThread: 0x600003b828c0>{number = 1, name = main}
sync-end
*/
复制代码
我们已经看到结果,任务按照顺序执行,内部 sync
会阻塞外部 sync
我们也会清楚,问题是在外部的 sync {}
还没有执行完的时候,为什么内部的 sync
可以执行?
首先要了解最重要的一点,那就是,为什么在串行队列中内部的 sync {}
无法执行?最重要的原因在于串行队列同时只能执行一个任务,所以在它上一个任务 ( 外部 sync
) 还没有执行完成之前,它是不能执行下一个任务 ( 内部 sync
) 的。
而并发队列就不同了,并发队列可以同时执行多个任务。也就是说,内部的 sync
已经不用等待外部 sync
执行完成就可以执行了。但是由于是同步任务,所以还是会等待,等待内部 sync
执行完成后,外部的 sync
继续执行。
请注意这里的执行和上面所说的,不存在一条线程同时执行多个任务的情况并不矛盾。因为在执行内部 sync
时,外部线程就停止操作了 ( 其实是转去执行内部 sync
了 ) ,如果是在执行内部 sync
的同时,外部的 sync
还在继续执行操作,那才叫同时。
因为 sync
都在一个线程 ( 主线程 ) 上,所以当你指定任务为 sync
时,主线程就知道接下来要去执行 sync
任务了,等执行完这个 sync
后再执行其他操作。例如,你可以把 sync
想象成是一个方法:
let queue = DispatchQueue(label: "concurrent.com", attributes: .concurrent)
queue.sync {
print("sync-start")
queueSync()
print("sync-end")
}
// 相当于之前的 queue.sync {}
func queueSync() {
(0..<5).forEach {
print("task \($0): \(Thread.current)")
Thread.sleep(forTimeInterval: 0.5)
}
}
复制代码
关于先进先出 (FIFO)
对串行队列来说,先进先出的意思很好理解,先进先出就是,先进去的一定先执行。当我们要执行一些任务时,这些任务就被存储在它的队列中,当线程进入到任务代码块时,就一定会先把这个任务执行完,再将任务出列,等这个任务出列后,线程才能继续去执行下一个任务。
那对于并发队列也是一样,当不同的线程同时进入到任务代码块时,就一定会先把这些任务执行完,再将这些任务出列,然后这些线程才能继续去执行其他任务。
示例9 - 关于并发的个数和线程性能
let queue = DispatchQueue(label: "concurrent.com", attributes: .concurrent)
(0..<100).forEach { i in
queue.async { print("\(i) \(Thread.current)") }
}
复制代码
会怎么样?答案是不会怎么样,只是会开启很多线程来执行这些异步任务。前面说过,每一个异步任务都是在不同的线程上执行的,那如果同时执行很多异步任务的话,像我们这里,同时开启 100 个异步任务,难道就系统就开辟 100 个线程来分别执行吗?也不是没有可能,这取决于你的 CPU,如果在 App 运行时,系统所能承载的最大线程个数为 10,那就会开辟这 10 条线程来重复执行任务,一次执行 10 个异步任务。
如果开辟的线程上限,那么剩下的那些任务就暂时无法执行,只能等到前面那些异步任务的线程执行完后,再去执行后面的异步任务。
总之一句话就是重复利用,先执行完的去执行还没有开始执行的,如果开辟的线程超出限制,那后面的任务就要等待前面的线程执行完才能执行。
但是如果开辟很多线程的话,会不会对我们的应用程序有负的影响?答案是一定的,开辟一条线程就要消耗一定的内存空间和系统资源,如果同时存在很多线程的话,那本身留给应用程序的内存就少得可怜,应用程序在运行时就会很卡,所以并不是线程开得越多越好,需要开发者自己平衡。
示例10 - DispatchQueue.global(_:) 全局并发队列
除了串行主队列外,系统还为我们创建了一个全局的并发队列 ( DispatchQueue.global()
) ,如果不想自己创建并发队列,那就用系统的 ( 我们一般也是用系统的 ) 。
DispatchQueue.global().async {
print("global async start \(Thread.current)")
DispatchQueue.global().sync {
(0..<5).forEach {
print("roop\($0) \(Thread.current)")
Thread.sleep(forTimeInterval: 0.2)
}
}
print("global async end \(Thread.current)")
}
/**
global async start <NSThread: 0x600002085300>{number = 3, name = (null)}
roop0 <NSThread: 0x600002085300>{number = 3, name = (null)}
roop1 <NSThread: 0x600002085300>{number = 3, name = (null)}
roop2 <NSThread: 0x600002085300>{number = 3, name = (null)}
roop3 <NSThread: 0x600002085300>{number = 3, name = (null)}
roop4 <NSThread: 0x600002085300>{number = 3, name = (null)}
global async end <NSThread: 0x600002085300>{number = 3, name = (null)}
*/
复制代码
和主队列一样,它的特殊之处在于,即使是用 sync
,任务也会在其他线程执行,至于它在哪一条线程执行,我猜测是它一定会让执行外部 async
的这条线程来执行,因为 sync
就是会让线程暂停执行后续操作,等到 sync
执行完后再接着执行,也就是说,在这种情况下,它只能顺序执行,那似乎只要一条线程就足够了,没有必要再开辟新线程来执行内部的 sync
。
另外,全局并发队列只有一个,并不是调用一次系统就创建一个,经过测试,它们是相等的:
let queue1 = DispatchQueue.global()
let queue2 = DispatchQueue.global()
if queue1 == queue2 { print("相等") }
// Prints "相等"
复制代码
总结
在前面的示例中,有关概念都是跟随示例引申出来的,讲得不是那么统一,在这里就总结一下。
-
队列
-
串行队列 在串行队列中执行任务时,任务按固定顺序执行,只能执行完一个任务后,再继续执行下一个任务 ( 这意味着串行队列同时只能执行一个任务 ) 。
-
并发队列
并发队列可以同时执行多个任务,任务并不一定按顺序执行,先执行哪几个任务由系统自动分配决定,等到有某个任务执行完后,就将这个任务出列,然后线程才能继续去执行其他任务。
-
-
任务
-
同步任务
不管是串行还是异步队列,只要是同步任务,就在主线程执行 (
DispatchQueue.global().sync
例外 ) 。同步任务会阻塞当前线程,让当前线程只能等待它执行完毕后才能执行。
在串行队列中,任务嵌套了
sync {}
的话会导致死锁。 -
异步任务
不论是串行还是异步队列,只要是异步任务,就在其他线程执行 (
DispatchQueue.main.sync
例外 ) ,不同的是串行队列在执行异步任务时,只会开辟一条线程,而并发队列在执行异步任务时,可以开辟多条线程。异步任务不会阻塞当前线程,线程不用等待异步任务执行完成就可以继续执行其他任务/操作。
异步任务不会产生死锁。
-