内容简介:Dispatch ( 全称 Grand Central Dispatch,简称 GCD ) 是一套由 Apple 编写以提供让代码以多核并发的方式执行应用程序的框架。在使用它之前,我们得先了解一下基本概念,我会先简单介绍,后面再根据讲解的内容逐步详细介绍,目的是为了方便读者融入。
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
例外 ) ,不同的是 串行队列在执行异步任务时,只会开辟一条线程,而并发队列在执行异步任务时,可以开辟多条线程 。异步任务不会阻塞当前线程,线程不用等待异步任务执行完成就可以继续执行其他任务/操作。
异步任务不会产生死锁。
-
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Java 线程和多线程执行过程分析
- Linux 线程(进程)数限制分析
- 【深入浅出-VisualVM】(5): 分析线程
- 【开发小记】 Java 线程池 之 被“吃掉”的线程异常(附源码分析和解决方法)
- Redis 6.0 IO线程功能分析
- JStorm 源码分析 - 异步循环线程 AsyncLoopThread
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。