前言:
Dispatch(Grand Central Dispatch)(超级中二的命名
与pthread和Thread不同的是,GCD增加了两个很重要的概念,任务和队列,它是iOS多线程的核心框架.
任务(Work Item, source等
任务就是要执行的代码段,很容易理解,GCD提供了很多任务的形式,比如DispatchWorkItem,DispatchSource,DispatchGroup,或者是一个block中的代码.队列(queue
队列指的是任务的队列,或者叫调度队列(Dispatch queue),但这个是抽象的概念,并不是数据结构上的队列,不仅可以存放任务,还可以指定任务如何执行,队列有两种,串行队列DISPATCH_QUEUE_SERIAL和并发队列DISPATCH_QUEUE_CONCURRENT.
队列永远都是先进先出,串行会等待前面的任务执行完,再执行下一个任务,并发的主旨是让任务一起执行,具体能不能做到还有其他限制,但是并发仍然是先进的先执行,只不过不会等待前面的任务结束,并且无法得知某个任务什么时候开始,什么时候结束,这些都取决于GCD自身.同步和异步(sync和async
只有串行队列和并发队列还不能决定任务如何执行,还需要指定是否创建线程,也就是指定同步sync还是异步async.同步不会开启新的线程,异步一定会开启新线程.
a.串行队列指定任务需要一个个执行,如果指定同步sync进行这个队列,则会在当前的线程中执行;
b.如果串行队列指定异步async进行,由于线程是cpu最小调度单位,没办法在两个任务之间调度,那GCD只好开启一个新的线程来进行这个队列,之后就和a是一样的逻辑了;
c.并发队列指定任务同时进行,如果指定sync,那么就不会生效
d.如果并发队列指定异步进行,那么GCD会根据任务数量创建一个或多个新的线程同时执行,当然线程的数量还会受到GCD底层实现的限制,一方面如果线程太多,来一个任务就开一个线程,还要队列干嘛,另一方面太多线程占用更多资源,一般最多是64个.
不管是主线程还是其他线程,一般都不会直接操作,需要关注的是任务本身,而管理任务的则是队列,同步还是异步是任务的执行策略,而执行策略决定会不会开启线程.
- 并发与并行:
并行指任务同时执行,是系统的行为,并发指的是代码的设计,希望代码的不同部分能够同时执行,是不是真的能"同时"执行取决于系统,多核可以真正的同时执行,而CPU自己也可以通过快速切换上下文来"同时"执行多个任务;虽然能写出并发的代码,但是是不是真的并行还要看GCD自己;并行的前提是并发,并发不能保证并行.
线程是cpu调度的方式,cpu只能处理一个任务,一个线程最多占用cpu几毫秒的时间,之后cpu会调度到其他线程,如果任务没执行完,之后还会调度回来继续执行.
一:基本使用
本篇讲的是swift的Dispatch库,大多数东西和OC的Dispatch相差不大,但是也优化了一些东西.
1.主线程(主队列)
主线程是唯一能够更新UI的线程,通常说的主线程其实指的是主队列(DispatchQueue.main),它是一个串行队列,里面的任务都会在主线程中串行执行;
GCD使用线程池来管理线程, 除了主队列只会在主线程进行之外(一开始就创建好了),任何任务都不能确定在哪个线程上进行;当然可以在运行的过程中查看当前线程.
就像前面说的,同步和异步决定是否开启线程,同步任务会在当前线程等待别的任务完成,如果添加一个同步的任务,并且指定在主队列中,会造成主队列的死锁.
但是这个现象不是特例,这个过程可以简化成这样:
DispatchQueue.global().async {
let queue = DispatchQueue.init(label: "test")
queue.sync {//A
print("aaa")
queue.sync {//B
print("bbb")
}
}//c
}
为了和主线程区分开,这段代码先创建了一个线程,假设叫T,里面的代码不在主线程运行;
然后创建了一个串行队列queue,添加了一个同步的任务A,于是A会在线程T中执行,在A中又添加了一个同步任务B到同一个队列中,B会在A执行完后再执行,也就是走完c的位置,但是c的位置依赖于B的代码走完,于是就堵在了这里;简单来说就是A执行了一半把B塞了进去,他俩在同一个线程中谁都不能先完成.
再重新思考主队列的死锁,如果同步任务的代码写在viewDidLoad里,那么主队列的任务要在viewDidLoad至少走完才会结束,添加在viewDidLoad中间的同步任务和viewDidLoad本身互相等待造成死锁.
DispatchQueue.global().async {
let queue = DispatchQueue.init(label: "test")
queue.sync {//A
print("aaa")
}//c
queue.sync {//B
print("bbb")
}
}
如果把任务B放在c外面,就没有问题
DispatchQueue.global().async {
let queue = DispatchQueue.init(label: "test")
queue.sync {//A
print(Thread.current)
let queue2 = DispatchQueue.init(label: "test")
queue2.sync {//B
print(Thread.current)
}
}//c
}
如果把B放在另一个队列queue2,也不会有问题,A和B仍然都在线程T中执行(两个print输出的结果一样),会按照添加的顺序执行.
DispatchQueue.global().async {
let queue = DispatchQueue.init(label: "test")
queue.sync {//A
print(Thread.current)
queue.async{//B
print(Thread.current)
}
}//c
}
再或者把B异步添加到队列queue,也能正常执行,这时gcd会开启新线程,两个print结果不一样.
DispatchQueue.global().async {
print("0.\(Thread.current)")
let queue = DispatchQueue.init(label: "test", qos: .background, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
queue.sync {//A
print("1.\(Thread.current)")
queue.sync{//B
print("2.\(Thread.current)")
}
}//c
}
最后一种,把queue换成并发队列,同步执行不会开启线程,0,1,2都是同一个线程,但是不会死锁,并发队列可以看出多轨道,但是线程只能只能执行一个任务,如果只有一个线程,就只好一个个的执行,但是不会造成第一个例子的那种阻塞.
总结一下就是,同步串行再加上任务重叠,就会造成死锁
- 前言说到串行队列添加异步任务会开启新的线程,但是主线程例外,主队列的任务只会运行在主线程.
DispatchQueue.init(label: "q1").async {
print("aaa -- \(Thread.current)")
}
DispatchQueue.main.async {
print("bbb -- \(Thread.current)")
}
DispatchQueue.init(label: "q2").async {
print("ccc -- \(Thread.current)")
}
aaa和ccc会开启新的线程,而bbb不会,并且子线程的执行速度还更快,因为main.async里的任务要等viewDidLoad走完.
DispatchQueue.main.async {
print("1:\(Thread.current)")
let queue = DispatchQueue.init(label: "q2")
queue.sync {
print("2:\(Thread.current)")
}
}
print("3:\(Thread.current)")
可以看到3先输出,也就是viewDidLoad先走完了,之后1和2相继执行.
2.全局队列
GCD队列,系统队列编号有11个,1为主队列,2为管理队列,3保留;4-11为8个全局队列,有四种优先级(quality-of-service) ,对应文档里从上到下是优先级从高到低,不过还有一个default和一个暂不指定unspecified
这些队列是系统提供的,初始化其实是获取而非创建,而且可能会有系统的任务在里面,因此开发者的代码不是其中唯一的任务.
DispatchQueue.global(),可以获取一个默认的全局队列.
DispatchQueue.global(qos:),获取一个指定优先级的全局队列
let queue2 = DispatchQueue.global(qos: DispatchQoS.QoSClass.background)
queue2.async {
print("2.\(Thread.current)")
}
let queue = DispatchQueue.global(qos:DispatchQoS.QoSClass.userInteractive)
queue.async {
print("1.\(Thread.current)")
}
不同优先级的全局队列,cpu调度的优先度不同,即便1写在后面,也是先执行
3.自定义队列
事实上,自定义的队列最终都会指向系统创建的队列,虽然是init,但其实是获取已经存在的或者系统在需要的时候创建的队列,之所以这么做,是为了方便管理任务,或者说给系统队列取个别名.
let queue = DispatchQueue.init(label: "test")
自定义一个串行队列
let queue = DispatchQueue.init(label: "test", qos: .background, attributes: .concurrent, autoreleaseFrequency: . workItem, target: nil)
自定义一个并发队列
- label是取个名字
- qos参数和获取全局队列是一样的,
- attributes有两个值, concurrent(创建一个并发队列), initiallyInactive(需要手动调用activate触发),如果传nil,则返回串行队列,并且可以传数组[concurrent,initiallyInactive]
- autoreleaseFrequency是任务相关的自动释放,有三个值,inherit跟随后面的target队列,后面再说; workItem按照任务的周期,取决于任务本身; never不主动释放,需要手动管理
- target是获取已存在的队列,这个目标队列决定了最终返回的队列的属性.
4.任务运行在哪个线程
前言提到了队列是开发者关注的重点,队列的属性是串行或者并发;
同步还是异步是任务的执行策略,这其中还好包含优先级等属性,在下一篇会说到;
let queue = DispatchQueue.init(label: "aaa")
for i in 0 ..< 100{
if i % 2 == 0{
queue.async {
sleep(2)
print("\(i): even -- async -- \(Thread.current)")
}
}else{
queue.sync {
sleep(1)
print("\(i): uneven -- sync -- \(Thread.current)")
}
}
}
这个例子创建一个串行队列,循环100次,偶数时向队列添加异步任务,奇数添加同步任务,看一下控制台输出
可以看到一定是按照顺序输出的,串行队列规定后添加的任务要在前面的任务完成后才开始,同步和异步只决定是否开启新的线程,并且可以看到同步的任务当前线程执行(这里是主线程),异步的在其他线程,具体是哪一个线程,由GCD决定,每一次的运行不一定会相同.
override func viewDidLoad() {
super.viewDidLoad()
let queue = DispatchQueue.init(label: "aaa")
queue.sync {
print(Thread.current)
}
}
所谓当前的线程,主要看任务如何被添加到队列,任务(代码)一定是写在函数里的,至少要在函数里被调用,例如上面这段代码,直接写viewDidLoad(),viewDidLoad运行在主线程,如果这时添加一个同步任务,不管是添加到那个队列里去(主队列自然不行),代码运行到这,所在的线程就是主线程;
DispatchQueue.global().async {
print("1:\(Thread.current)")
let queue = DispatchQueue.init(label: "q2")
queue.sync {
print("2:\(Thread.current)")
}
}
print("3:\(Thread.current)")
同样的,上面是一个异步的代码块,在其中添加了一个同步任务,因此1和2是同一个线程,3又回到了主线程
let queue = DispatchQueue.init(label: "aaa", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)
for i in 0 ..< 100{
queue.async {
sleep(3)
print("\(i): even -- async -- \(Thread.current)")
}
}
这段代码运行时,先等待了3秒,然后飞快的输出了100次,可以看到开启了很多线程
let queue = DispatchQueue.init(label: "aaa", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)
for i in 0 ..< 100{
if i % 2 == 0{
queue.async {
print("\(i): even -- async -- \(Thread.current) -- \(Int(Date.init().timeIntervalSince1970))")
}
}else{
queue.sync {
print("\(i): uneven -- sync \(Thread.current) -- \(Int(Date.init().timeIntervalSince1970))")
}
}
}
把上面的代码改造一下 ,单数同步,偶数异步,仍然是飞快的输出
let queue = DispatchQueue.init(label: "aaa", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)
for i in 0 ..< 100{
if i % 2 == 0{
queue.async {
print("\(i): even -- async -- \(Thread.current) -- \(Int(Date.init().timeIntervalSince1970))")
}
}else{
queue.sync {
sleep(2)
print("\(i): uneven -- sync \(Thread.current) -- \(Int(Date.init().timeIntervalSince1970))")
}
}
}
再改造一下,给同步的任务添加一个耗时2秒,再运行;
现在发现两秒跳一次,说明异步在等待同步,为什么异步没有一瞬间执行完,让同步自己去慢慢跑呢:
并发队列和串行队列都是先进先出,只不过并发队列里的任务不用等待前面的任务执行完,但是要等前面的任务开始了,后面的才能开始;
同步的任务这里在主线程执行,主线程就一个,线程也只能做一件事,做完了才能做其他事,前面异步任务一瞬间完成,是因为开了很多个线程;
这个例子i=3的同步任务要等待i=1完成之后才能进行,而i=3不开始,456...也不能开始,所以后面的任务虽然是异步的,队列也是并发的,但是却被迫等待前面的任务完成.
// DispatchQueue.global().async {
DispatchQueue.init(label: "q", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil).async {
let queue = DispatchQueue.init(label: "aaa", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)
for i in 0 ..< 100{
if i % 2 == 0{
queue.async {
// sleep(3)
print("\(i): even -- async -- \(Thread.current) -- \(Int(Date.init().timeIntervalSince1970))")
}
}else{
queue.sync {
sleep(2)
print("\(i): uneven -- sync \(Thread.current) -- \(Int(Date.init().timeIntervalSince1970))")
}
}
}
}
// }
这个例子在最外面套上DispatchQueue.global().async {} 或者再自定义一个异步也是一样的,主线程会变成另一个线程,但是仍然要等待.因为async {}里的线程已经确定了,而且就一个.