赞
踩
Grand Central Dispatch简称GCD,苹果官方推荐给开发者使用的首选多线程解决方案。多线程开发涉及的细节非常多,下面我会用例子细致的讲解GCD,请一定要精读,一定要用Xcode或Playground多次运行代码去对比结果。实践出真知,练习完这篇文章,你一定会觉得精通Swift多线程原来很简单。
本文前半部分,我会尽可能精简话语,降低入门门槛,随着理解的深入,后面我会循序渐进地讲详细一些。
调度工作项:其实就是一项任务,可以把你想要执行的代码写成闭包,在DispatchWorkItem初始化时传进去,方便后续管理任务,并且会让代码更整洁。
官网原文:The work you want to perform, encapsulated in a way that lets you attach a completion handle or execution dependencies.
调度工作项初始化,正常情况下,使用第一种方式即可(特殊情况后续会再讲解):
- //1. 只带尾随闭包
- let item1 = DispatchWorkItem {
- print("item1")
- }
-
- //2. 指定qos(执行优先级)或flags(特殊行为标记)
- let item2 = DispatchWorkItem(qos: .userInteractive, flags: .barrier) {
- print("item2")
- }
- 复制代码
调度队列:一个对象,用来管理任务在app的主线程或后台线程串行或并行执行。
官网原文:An object that manages the execution of tasks serially or concurrently on your app's main thread or on a background thread.
DispatchQueue有三种类型:
3.1 Main queue(主队列,串行)
Main queue与主线程关联的调度队列,是一种串行队列(Serial),与UI相关的操作必须放在Main queue中执行,获取方式是:
- let mainQueue = DispatchQueue.main
- 复制代码
3.2 Global queue(全局队列,并行)
Global queue运行在后台线程,是系统内共享的全局队列,是一种并行队列(Concurrent),用于处理并发任务,获取方式是:
- let globalQueue = DispatchQueue.global()
- 复制代码
3.3 Custom queue(自定义队列,默认串行)
Custom queue运行在后台线程,默认是串行队列(Serial),初始化时指定attributes参数为 .concurrent,可以创建成并行队列(Concurrent),创建方式如下:
- //串行队列,label名字随便取
- let serialQueue = DispatchQueue(label: "test")
-
- //并行队列
- let concurrentQueue = DispatchQueue(label: "test", attributes: .concurrent)
- 复制代码
调度组:一个小组,你可以把多项任务放到一个组里,方便进行统一管理(直译过来并不好理解)。
官网原文:A group of tasks that you monitor as a single unit.
DispatchGroup可以很方便的管理多项任务。比如当同一组里的所有事件都完成后,GCD API可以发送通知,执行相应的操作。常用方法:
新建Playground项目,定义四个调度任务,提供给下文调用,可大幅降低下文代码量,部分运行结果请自己复制代码多次运行感受,我只讲结果:
- import Foundation
-
- //定义四个调度任务,打印当前线程数据
- let item1 = DispatchWorkItem {
- for i in 0...4{
- print("item1 -> \(i) thread: \(Thread.current)")
- }
- }
-
- let item2 = DispatchWorkItem {
- for i in 0...4{
- print("item2 -> \(i) thread: \(Thread.current)")
- }
- }
-
- let item3 = DispatchWorkItem {
- for i in 0...4{
- print("item3 -> \(i) thread: \(Thread.current)")
- }
- }
-
- let item4 = DispatchWorkItem {
- for i in 0...4{
- print("item4 -> \(i) thread: \(Thread.current)")
- }
- }
- 复制代码
5.1 异步执行
- //主队列追加异步任务,按顺序打印
- let mainQueue = DispatchQueue.main
- mainQueue.async(execute: item1)
- mainQueue.async(execute: item2)
- mainQueue.async(execute: item3)
- mainQueue.async(execute: item4)
-
- //全局队列追加异步任务,随机打印
- let globalQueue = DispatchQueue.global()
- globalQueue.async(execute: item1)
- globalQueue.async(execute: item2)
- globalQueue.async(execute: item3)
- globalQueue.async(execute: item4)
-
- //自定义串行队列追加异步任务,按顺序打印
- let serialQueue = DispatchQueue(label: "serial")
- serialQueue.async(execute: item1)
- serialQueue.async(execute: item2)
- serialQueue.async(execute: item3)
- serialQueue.async(execute: item4)
-
- //自定义并行队列追加异步任务,随机打印
- let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
- concurrentQueue.async(execute: item1)
- concurrentQueue.async(execute: item2)
- concurrentQueue.async(execute: item3)
- concurrentQueue.async(execute: item4)
- 复制代码
注:在串行队列中执行异步任务,结果跟执行同步任务完全一样
5.2 同步执行
- //主队列追加同步任务,会引起死锁
- let mainQueue = DispatchQueue.main
- mainQueue.sync(execute: item1)
- mainQueue.sync(execute: item2)
- mainQueue.sync(execute: item3)
- mainQueue.sync(execute: item4)
-
- //全局队列追加同步任务,按顺序打印
- let globalQueue = DispatchQueue.global()
- globalQueue.sync(execute: item1)
- globalQueue.sync(execute: item2)
- globalQueue.sync(execute: item3)
- globalQueue.sync(execute: item4)
-
- //自定义串行队列追加同步任务,按顺序打印
- let serialQueue = DispatchQueue(label: "serial")
- serialQueue.sync(execute: item1)
- serialQueue.sync(execute: item2)
- serialQueue.sync(execute: item3)
- serialQueue.sync(execute: item4)
-
- //自定义并行队列追加同步任务,按顺序打印
- let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
- concurrentQueue.sync(execute: item1)
- concurrentQueue.sync(execute: item2)
- concurrentQueue.sync(execute: item3)
- concurrentQueue.sync(execute: item4)
- 复制代码
注:在并行队列中执行同步任务,跟在串行队列中执行异步或同步任务,结果完全一样。 在主队列中不能混入同步任务,否则会引起死锁。
5.3 同步异步混合执行
- //主队列同步异步混合,会引起死锁
- let mainQueue = DispatchQueue.main
- mainQueue.sync(execute: item1)//同步任务
- mainQueue.async(execute: item2)
- mainQueue.async(execute: item3)
- mainQueue.async(execute: item4)
-
- //全局队列同步异步混合,同步任务按顺序打印,异步任务随机打印
- //本例中同步任务执行完,才会执行后续的异步任务
- let globalQueue = DispatchQueue.global()
- globalQueue.sync(execute: item1)//同步任务
- globalQueue.async(execute: item2)
- globalQueue.async(execute: item3)
- globalQueue.async(execute: item4)
-
- //自定义串行队列同步异步混合,按顺序打印
- let serialQueue = DispatchQueue(label: "serial")
- serialQueue.sync(execute: item1)//同步任务
- serialQueue.async(execute: item2)
- serialQueue.async(execute: item3)
- serialQueue.async(execute: item4)
-
- //自定义并行队列同步异步混合,同步任务按顺序打印,异步任务随机打印
- //本例中同步任务执行完,才会执行后续的异步任务
- let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
- concurrentQueue.sync(execute: item1)//同步任务
- concurrentQueue.async(execute: item2)
- concurrentQueue.async(execute: item3)
- concurrentQueue.async(execute: item4)
- 复制代码
注:在并行队列中执行同步任务,跟在串行队列中执行异步或同步任务,结果完全一样。 在主队列中不能混入同步任务,否则会引起死锁。
6.1 主队列死锁
上文提到了主队列不能混入同步任务,否则会引起死锁,为何呢?因为主队列是串行队列,并且仅能运行在主线程上,它无法去创建新的线程,也就意味着所有的代码都必须在只能在一个线程上运行。
正常情况下,主队列上存在源源不断的异步任务(比如用来不断刷新UI的任务,用A表示),如果混入同步任务(用B表示),如果B在A之后,从时间上看,B执行完才能执行A;而从空间上看,A执行完才能执行B。两个任务都很有礼貌,相互等待、相互谦让,谁也不好意思先执行,于是就引起了死锁,导致程序卡死崩溃。
官网原文:Attempting to synchronously execute a work item on the main queue results in deadlock.
- //会引起死锁
- let mainQueue = DispatchQueue.main
- mainQueue.async(execute: item1)
- mainQueue.async(execute: item2)
- mainQueue.async(execute: item3)
- mainQueue.sync(execute: item4)//同步任务
- 复制代码
有人可能会想,如果A在B之后呢?是不是就不会引起死锁?看起来不会死锁,可惜Playground运行这样的代码,每次都崩溃,应该是程序刚运行,主队列就存在我们看不到的异步任务。
- //依然会引起死锁
- let mainQueue = DispatchQueue.main
- mainQueue.sync(execute: item1)//同步任务
- mainQueue.async(execute: item2)
- mainQueue.async(execute: item3)
- mainQueue.async(execute: item4)
- 复制代码
因此只能认为:主队列上不能存在同步任务,否则一定会引起死锁。
6.2 其他队列死锁
上文提到主队列死锁,那其他类型的队列会不会引起死锁呢?下面来试一下:
- let serialQueue = DispatchQueue(label: "serial")
- //死锁
- serialQueue.sync {
- print("同步执行 thread: \(Thread.current)")
- serialQueue.sync {
- print("同步执行 thread: \(Thread.current)")
- }
- }
- //死锁
- serialQueue.async {
- print("异步执行 thread: \(Thread.current)")
- serialQueue.sync {
- print("同步执行 thread: \(Thread.current)")
- }
- }
- //不会引起死锁
- serialQueue.sync {
- print("同步执行 thread: \(Thread.current)")
- serialQueue.async {
- print("异步执行 thread: \(Thread.current)")
- }
- }
- //不会引起死锁
- serialQueue.async {
- print("异步执行 thread: \(Thread.current)")
- serialQueue.async {
- print("异步执行 thread: \(Thread.current)")
- }
- }
- 复制代码
- //自定义并行队列(全局并行队列结果一样)
- let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
- //不会引起死锁
- concurrentQueue.async {
- print("异步执行 thread: \(Thread.current)")
- concurrentQueue.sync {
- print("同步执行 thread: \(Thread.current)")
- }
- }
- //不会引起死锁
- concurrentQueue.sync {
- print("同步执行 thread: \(Thread.current)")
- concurrentQueue.sync {
- print("同步执行 thread: \(Thread.current)")
- }
- }
- //不会引起死锁
- concurrentQueue.sync {
- print("同步执行 thread: \(Thread.current)")
- concurrentQueue.async {
- print("异步执行 thread: \(Thread.current)")
- }
- }
- //不会引起死锁
- concurrentQueue.async {
- print("异步执行 thread: \(Thread.current)")
- concurrentQueue.async {
- print("异步执行 thread: \(Thread.current)")
- }
- }
- 复制代码
6.3 死锁总结
通过上文可以看到,自定义串行队列嵌套同步任务,也是可以引起死锁的,所以死锁不是主队列的专利。但为什么会引起死锁,核心原因是什么?运行下面的代码看看结果:
- print("=> 开始执行")
-
- let mainQueue = DispatchQueue.main
- mainQueue.async(execute: item1)//异步任务
-
- print("=> 执行完毕1")
-
- let globalQueue = DispatchQueue.global()
- globalQueue.sync(execute: item2)//同步任务
-
- print("=> 执行完毕2")
-
- let serialQueue = DispatchQueue(label: "serial")
- serialQueue.sync(execute: item3)//同步任务
-
- print("=> 执行完毕3")
-
- let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
- concurrentQueue.sync(execute: item4)//同步任务
-
- print("=> 执行完毕all")
-
- 运行结果:
- => 开始执行
- => 执行完毕1
- item2 -> 0 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- item2 -> 1 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- item2 -> 2 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- item2 -> 3 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- item2 -> 4 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- => 执行完毕2
- item3 -> 0 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- item3 -> 1 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- item3 -> 2 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- item3 -> 3 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- item3 -> 4 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- => 执行完毕3
- item4 -> 0 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- item4 -> 1 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- item4 -> 2 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- item4 -> 3 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- item4 -> 4 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- => 执行完毕all
- item1 -> 0 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- item1 -> 1 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- item1 -> 2 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- item1 -> 3 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- item1 -> 4 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
- 复制代码
看出什么问题没有?四组代码的运行结果完全一样,连线程信息也都一摸一样,都是运行在主线程上(main thread),并且第一组的代码放在了最后执行。也就是说:
- 主队列上的所有任务(只有可能是异步任务)和其他队列的同步任务都运行在主线程上(主线程有且只有一个)。
- 线程不在乎任务是同步还是异步,只有队列才在乎。
- 线程不会死锁,只有队列才会死锁。
主队列添加同步任务会造成死锁的根本原因是:
- 主队列只能运行在主线程(重要的事情再说一遍)。
- 主队列没有本事开启后台线程去干别的事情。
- 主队列一旦混入同步任务,就会跟已经存在的异步任务相互等待,导致死锁。
自定义串行队列添加同步任务不会死锁,因为:
自定义串行队列有能力启动主线程和后台线程(只能启动一个后台线程)。 自定义串行队列遇到同步任务,会自动安排在主线程执行;遇到异步任务,自动安排在后台线程执行,所以不会死锁。
并行队列添加同步任务不会死锁,因为:
并行队列有能力启动主线程和后台线程(可以启动一个或多个后台线程,部分设备上可以启动多达64个后台线程)。 并行队列遇到同步任务,会自动安排在主线程执行;遇到异步任务,自动安排在后台线程执行,所以不会死锁。
自定义串行队列一个异步或同步任务(A)嵌套另一个同步任务(B)会引起死锁,因为:
A、B任务等效为:A1 -> B -> A2,B是同步任务,B在A1之后、A2之前,B必须等A2执行完才能执行,A2必须等B执行完才能执行,A2执行完才算A执行完了,逻辑上已经陷入死循环,两者相互等待,导致死锁。所以,串行队列不能嵌套同步任务,否则会引起死锁。
7.1 背景介绍
这一章来模拟网络请求:在APP中请求网络数据(任务A: 耗时10s),获取数据后进行一定的处理(任务B: 耗时5s),最后刷新UI。
假如A和B都是同步任务,放主队列会死锁,而放其他任何队列,界面都会卡死15s,如果不信,把下面代码里的两种线程休眠方法(二选一,其实不止这两种),放在APP UIViewController里试试:
- override func viewDidAppear(_ animated: Bool) {
- //1. 全局队列执行同步任务
- DispatchQueue.global().sync {
- sleep(15)//当前线程休眠15秒
- }
- //2. 主队列执行异步任务
- DispatchQueue.main.async {
- sleep(15)//当前线程休眠15秒
- }
- }
- 复制代码
不出所料,两种方法,均让界面卡死15s。回想一下上文说过的:所有的同步任务最终都要安排到主线程运行,主线程运行长耗时任务都会导致界面严重卡顿,所以:
能异步执行的长耗时任务,千万不要同步执行。 长耗时同步任务欠下的债,都由界面来偿还。
假如A和B都是异步任务,即使这样,你也不能都放在主队列中处理,这样也会导致APP界面卡住15s,因为上面说到了:主线程运行长耗时任务都会导致界面严重卡顿。
所有的长耗时任务,千万不要放在主队列中执行。 主队列长耗时异步任务欠下的债,也都由界面来偿还。
说了那么多,你现在应该能够深切地理解各种队列的运行原理了。
7.2 网络请求实例
现在讲讲使用GCD多线程处理网络请求的正确做法:A、B都定义成异步任务,在并行队列中嵌套异步任务,最后切换到主队列去刷新UI,这样做界面可以保证最流畅。
- //创建并行队列,尽量用自定义队列,免得自己的代码质量不过关,影响全局队列
- let queue = DispatchQueue(label: "com.apple.request", attributes: .concurrent)
-
- //异步执行
- queue.async {
-
- print("开始请求数据 \(Date()) thread: \(Thread.current)")
- sleep(10)//模拟网络请求
- print("数据请求完成 \(Date()) thread: \(Thread.current)")
-
- //异步执行
- queue.async {
- print("开始处理数据 \(Date()) thread: \(Thread.current)")
- sleep(5)//模拟数据处理
- print("数据处理完成 \(Date()) thread: \(Thread.current)")
-
- //切换到主队列,刷新UI
- DispatchQueue.main.async {
- print("UI刷新成功 \(Date()) thread: \(Thread.current)")
- }
- }
- }
-
- //运行结果
- 开始请求数据 2020-08-06 06:40:57 +0000 thread: <NSThread: 0x7ff917d8c0c0>{number = 4, name = (null)}
- 数据请求完成 2020-08-06 06:41:07 +0000 thread: <NSThread: 0x7ff917d8c0c0>{number = 4, name = (null)}
- 开始处理数据 2020-08-06 06:41:07 +0000 thread: <NSThread: 0x7ff8f7d0c190>{number = 3, name = (null)}
- 数据处理完成 2020-08-06 06:41:12 +0000 thread: <NSThread: 0x7ff8f7d0c190>{number = 3, name = (null)}
- UI刷新成功 2020-08-06 06:41:12 +0000 thread: <NSThread: 0x7ff917c0e7e0>{number = 1, name = main}
- 复制代码
可以看到队列和线程均进行了预期的切换,GCD队列切换像俄罗斯套娃一样,一层一层的嵌套就行,等嵌套出问题了,去第6章死锁分析寻找原因进行修改即可。
如果希望多项任务执行完毕后,再去执行另一项任务,可以使用DispatchGroup。这些任务可以放在同一队列中,也可以放在不同队列中。
DispatchGroup常用的方法:
group.wait():阻塞当前线程,一直到group所有任务执行完毕。
group.notify():所有任务执行完毕后,异步发送通知,不阻塞当前线程。
8.1 使用group.notify()改写一下上一章网络请求的例子:
- let group = DispatchGroup()
- let queue = DispatchQueue(label: "com.apple.request", attributes: .concurrent)
-
- //异步执行
- queue.async(group: group) {
-
- print("开始请求数据 \(Date()) thread: \(Thread.current)")
- sleep(10)//模拟网络请求
- print("数据请求完成 \(Date()) thread: \(Thread.current)")
-
- //异步执行
- queue.async(group: group) {
- print("开始处理数据 \(Date()) thread: \(Thread.current)")
- sleep(5)//模拟数据处理
- print("数据处理完成 \(Date()) thread: \(Thread.current)")
- }
- }
-
- print("开始监听")
-
- //在当前队列监听
- group.notify(queue: queue) {
- //切换到主队列,刷新UI
- DispatchQueue.main.async {
- print("UI刷新成功 \(Date()) thread: \(Thread.current)")
- }
- }
-
- print("监听完毕")
-
- //运行结果
- 开始监听
- 监听完毕
- 开始请求数据 2020-08-06 06:45:22 +0000 thread: <NSThread: 0x7fe312f30b60>{number = 4, name = (null)}
- 数据请求完成 2020-08-06 06:45:32 +0000 thread: <NSThread: 0x7fe312f30b60>{number = 4, name = (null)}
- 开始处理数据 2020-08-06 06:45:32 +0000 thread: <NSThread: 0x7fe312e70d70>{number = 5, name = (null)}
- 数据处理完成 2020-08-06 06:45:37 +0000 thread: <NSThread: 0x7fe312e70d70>{number = 5, name = (null)}
- UI刷新成功 2020-08-06 06:45:37 +0000 thread: <NSThread: 0x7fe312c0e7e0>{number = 1, name = main}
- 复制代码
如你所愿,运行结果跟上文一致。
8.2 精简代码,直接在主队列监听通知、刷新UI:
- let group = DispatchGroup()
- let queue = DispatchQueue(label: "com.apple.request", attributes: .concurrent)
-
- //异步执行
- queue.async(group: group) {
-
- print("开始请求数据 \(Date()) thread: \(Thread.current)")
- sleep(10)//模拟网络请求
- print("数据请求完成 \(Date()) thread: \(Thread.current)")
-
- //异步执行
- queue.async(group: group) {
- print("开始处理数据 \(Date()) thread: \(Thread.current)")
- sleep(5)//模拟数据处理
- print("数据处理完成 \(Date()) thread: \(Thread.current)")
- }
- }
-
- print("开始监听")
-
- //切换到主队列监听,刷新UI
- group.notify(queue: DispatchQueue.main) {
- print("UI刷新成功 \(Date()) thread: \(Thread.current)")
- }
-
- print("监听完毕")
-
- //运行结果
- 开始监听
- 监听完毕
- 开始请求数据 2020-08-06 06:49:31 +0000 thread: <NSThread: 0x7fc608c80370>{number = 4, name = (null)}
- 数据请求完成 2020-08-06 06:49:41 +0000 thread: <NSThread: 0x7fc608c80370>{number = 4, name = (null)}
- 开始处理数据 2020-08-06 06:49:41 +0000 thread: <NSThread: 0x7fc608d2b200>{number = 5, name = (null)}
- 数据处理完成 2020-08-06 06:49:46 +0000 thread: <NSThread: 0x7fc608d2b200>{number = 5, name = (null)}
- UI刷新成功 2020-08-06 06:49:46 +0000 thread: <NSThread: 0x7fc608c0e7e0>{number = 1, name = main}
-
- 复制代码
如你所愿,运行结果依然一致。
8.3 使用group.wait()改写:
- let group = DispatchGroup()
- let queue = DispatchQueue(label: "com.apple.request", attributes: .concurrent)
-
- //异步执行
- queue.async(group: group) {
-
- print("开始请求数据 \(Date()) thread: \(Thread.current)")
- sleep(10)//模拟网络请求
- print("数据请求完成 \(Date()) thread: \(Thread.current)")
-
- //异步执行
- queue.async(group: group) {
- print("开始处理数据 \(Date()) thread: \(Thread.current)")
- sleep(5)//模拟数据处理
- print("数据处理完成 \(Date()) thread: \(Thread.current)")
- }
- }
-
- print("开始监听")
-
- //切换到主队列监听,刷新UI
- group.notify(queue: DispatchQueue.main) {
- print("UI刷新成功 \(Date()) thread: \(Thread.current)")
- }
-
- group.wait()//阻塞当前线程
-
- print("监听完毕")
-
- //运行结果
- 开始监听
- 开始请求数据 2020-08-06 06:53:00 +0000 thread: <NSThread: 0x7fe1ad538580>{number = 4, name = (null)}
- 数据请求完成 2020-08-06 06:53:10 +0000 thread: <NSThread: 0x7fe1ad538580>{number = 4, name = (null)}
- 开始处理数据 2020-08-06 06:53:10 +0000 thread: <NSThread: 0x7fe1b8010060>{number = 5, name = (null)}
- 数据处理完成 2020-08-06 06:53:15 +0000 thread: <NSThread: 0x7fe1b8010060>{number = 5, name = (null)}
- 监听完毕
- UI刷新成功 2020-08-06 06:53:15 +0000 thread: <NSThread: 0x7fe1ad40e7e0>{number = 1, name = main}
-
- 复制代码
可以看到group.wait()的确阻塞了当前线程。
在第7章的例子里,嵌套了三层,还不算多,但是已经可以隐约感受到嵌套地狱了。这一节用队列挂起、恢复重写,解决嵌套问题。以后遇到更多层级的嵌套,可以用同样的方法解决。
- let group = DispatchGroup()
- let queue1 = DispatchQueue(label: "com.apple.request", attributes: .concurrent)
- let queue2 = DispatchQueue(label: "com.apple.response", attributes: .concurrent)
-
- queue2.suspend()//队列挂起
-
- //异步执行
- queue1.async(group: group) {
- print("开始请求数据 \(Date()) thread: \(Thread.current)")
- sleep(10)//模拟网络请求
- print("数据请求完成 \(Date()) thread: \(Thread.current)")
-
- queue2.resume()//网络数据请求完成,恢复队列,进行数据处理
- }
-
- //异步执行
- queue2.async(group: group) {
- print("开始处理数据 \(Date()) thread: \(Thread.current)")
- sleep(5)//模拟数据处理
- print("数据处理完成 \(Date()) thread: \(Thread.current)")
- }
-
- print("开始监听")
-
- //切换到主队列监听,刷新UI
- group.notify(queue: DispatchQueue.main) {
- print("UI刷新成功 \(Date()) thread: \(Thread.current)")
- }
-
- print("监听完毕")
- 复制代码
如果有一个变量有可能被多个线程同时读写,结果便不可预期,必须进行特殊处理,来保证线程安全。
10.1 通过barrier标识设置屏障
自定义队列支持DispatchWorkItem设置flags为.barrier,可以支持barrier之前的任务全部执行完毕后,再执行.barrier任务,最后再执行.barrier之后的任务,这样处理可以保证线程安全。(注:全局队列,flags设置.barrier无效)
- import Foundation
-
- let item1 = DispatchWorkItem {
- for i in 0...4{
- print("item1 -> \(i) thread: \(Thread.current)")
- }
- }
-
- let item2 = DispatchWorkItem {
- for i in 0...4{
- print("item2 -> \(i) thread: \(Thread.current)")
- }
- }
-
- //给item3任务加barrier标识
- let item3 = DispatchWorkItem(flags: .barrier) {
- for i in 0...4{
- print("item3 barrier -> \(i) thread: \(Thread.current)")
- }
- }
-
- let item4 = DispatchWorkItem {
- for i in 0...4{
- print("item4 -> \(i) thread: \(Thread.current)")
- }
- }
-
- let item5 = DispatchWorkItem {
- for i in 0...4{
- print("item5 -> \(i) thread: \(Thread.current)")
- }
- }
-
-
- let queue = DispatchQueue(label: "test", attributes: .concurrent)
- queue.async(execute: item1)
- queue.async(execute: item2)
- queue.async(execute: item3)
- queue.async(execute: item4)
- queue.async(execute: item5)
-
- //运行结果
- item1 -> 0 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
- item2 -> 0 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
- item1 -> 1 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
- item2 -> 1 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
- item1 -> 2 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
- item2 -> 2 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
- item2 -> 3 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
- item1 -> 3 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
- item2 -> 4 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
- item1 -> 4 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
- item3 barrier -> 0 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
- item3 barrier -> 1 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
- item3 barrier -> 2 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
- item3 barrier -> 3 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
- item3 barrier -> 4 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
- item4 -> 0 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
- item5 -> 0 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
- item4 -> 1 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
- item4 -> 2 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
- item5 -> 1 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
- item4 -> 3 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
- item4 -> 4 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
- item5 -> 2 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
- item5 -> 3 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
- item5 -> 4 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
- 复制代码
10.2 使用DispatchSemaphore给线程上锁
DispatchSemaphore被很多人翻译成信号量,说实话我这辈子第一次听说信号量,信号还有量?什么量?多少量?
吐槽完毕,为了方便理解,在这里我把它临时翻译成红绿灯吧。
DispatchSemaphore初始化时只有一个参数value(通行数量),表示还可以通行几辆车(还可以执行几个异步任务)。
DispatchSemaphore有两个方法:
10.2.1 举一个99乘法表的例子,感受下DispatchSemaphore:
- let semaphore = DispatchSemaphore(value: 1)
- let queue = DispatchQueue(label: "concurrent", attributes: .concurrent)
- //执行9个异步任务
- for i in 1...9 {
- queue.async {
- semaphore.wait()//绿灯时间减1,此处变为0,红灯,全都得等着
- var str = ""
- for j in 1...9{
- //格式化一下字符串,后面加两个空格。如果只有个位数的,前面补个空格
- let value = i * j
- let tempStr = value <= 9 ? " \(value) " : "\(value) "
- str += tempStr
- }
- print(str)
- semaphore.signal()//绿灯时间加1,后面可继续通行
- }
- }
-
- //运行结果
- 1 2 3 4 5 6 7 8 9
- 2 4 6 8 10 12 14 16 18
- 3 6 9 12 15 18 21 24 27
- 4 8 12 16 20 24 28 32 36
- 5 10 15 20 25 30 35 40 45
- 6 12 18 24 30 36 42 48 54
- 7 14 21 28 35 42 49 56 63
- 8 16 24 32 40 48 56 64 72
- 9 18 27 36 45 54 63 72 81
- 复制代码
99乘法表显示理想
10.2.2 注释掉semaphore.wait()和semaphore.signal(),多运行几次试试看:
- let semaphore = DispatchSemaphore(value: 1)
- let queue = DispatchQueue(label: "concurrent", attributes: .concurrent)
- //执行9个异步任务
- for i in 1...9 {
- queue.async {
- //semaphore.wait()//绿灯时间减1,此处变为0,红灯,全都得等着
- var str = ""
- for j in 1...9{
- //格式化一下字符串,后面加两个空格。如果只有个位数的,前面补个空格
- let value = i * j
- let tempStr = value <= 9 ? " \(value) " : "\(value) "
- str += tempStr
- }
- print(str)
- //semaphore.signal()//绿灯时间加1,后面可继续通行
- }
- }
-
- //运行结果
- 5 10 15 20 25 30 35 40 45
- 4 8 12 16 20 24 28 32 36
- 3 6 9 12 15 18 21 24 27
- 1 2 3 4 5 6 7 8 9
- 8 16 24 32 40 48 56 64 72
- 9 18 27 36 45 54 63 72 81
- 2 4 6 8 10 12 14 16 18
- 6 12 18 24 30 36 42 48 54
- 7 14 21 28 35 42 49 56 63
- 复制代码
99乘法表已经失控
为了更深刻的理解,试试把上面的例1中DispatchSemaphore初始化时value设为2或3,多次运行下程序看看结果,你能感受到通行数量对失控程度的影响。
10.3 使用串行队列+计算属性,修改变量
- import Foundation
-
- let queue = DispatchQueue(label: "test")
-
- var a:Int = 10
- var b:Int{
- get{
- queue.sync {
- print("同步读取 thread = \(Thread.current)")
- return a
- }
- }
- set{
- queue.sync {
- print("同步写入 thread = \(Thread.current)")
- a = newValue
- }
- }
- }
-
- b = 30//赋值
-
- print("a = \(a) b = \(b) thread = \(Thread.current)")
-
- //运行结果
- 同步写入 thread = <NSThread: 0x7f8018c0e7e0>{number = 1, name = main}
- 同步读取 thread = <NSThread: 0x7f8018c0e7e0>{number = 1, name = main}
- a = 30 b = 30 thread = <NSThread: 0x7f8018c0e7e0>{number = 1, name = main}
- 复制代码
尝试修改set为异步写入,思索下结果。
DispatchQoS调度优先级:直译过来就是应用在任务上的服务质量或执行优先级,可以理解为任务的身份、等级。可以用来修饰DispatchWorkItem、DispatchQueue。
就像航空公司有身份的客户,在VIP休息室等飞机、坐头等舱、高质量空姐贴心服务等等,最好的服务优先都给你;如果你没有身份、只有身份证,平平安安的到达目的地就可以知足了;如果你连身份证也没有,那就去坐公交车吧。
官网原文:The quality of service, or the execution priority, to apply to tasks.
DispatchQoS有以下几种类型:
DispatchQoS其实只是一个简单的优先级标识,为何会放在进阶篇里说呢?
因为对于绝大部分开发者来说,没必要设置这个标识,设置了也只是徒增代码复杂度,花里胡哨的技巧用了一大堆,代码量不小,最后到处都是bug,有意义吗?
还是尽量让代码简单点、少出问题最好,很多书里都讲:代码越少,bug越少。当有一天你想增强用户体验、提高代码运行效率、优化设备能耗,说明你的应用质量、代码档次都已经很不错了,明显属于进阶水准,这时你应该去试试这个标识了。所以,鄙人认为,DispatchQoS属于进阶内容。
11.1 在DispatchWorkItem上添加DispatchQoS标识:
- import Foundation
-
- let item1 = DispatchWorkItem(qos: .userInteractive) {
- for i in 0...9999{
- print("--item1 -> \(i) thread: \(Thread.current)")
- }
- }
-
- let item2 = DispatchWorkItem(qos: .unspecified) {
- for i in 0...9999{
- print("item2 -> \(i) thread: \(Thread.current)")
- }
- }
-
- let queue = DispatchQueue(label: "test1", attributes: .concurrent)
- queue.async(execute: item1)
- queue.async(execute: item2)
- 复制代码
运行结果显示item1执行完了,item2才开始打印3824。
for循环次数需要调大一些,否则效果不明显。
11.2 在DispatchQueue上添加DispatchQoS标识:
- import Foundation
-
- let item1 = DispatchWorkItem {
- for i in 0...9999{
- print("--item1 -> \(i) thread: \(Thread.current)")
- }
- }
-
- let item2 = DispatchWorkItem {
- for i in 0...9999{
- print("item2 -> \(i) thread: \(Thread.current)")
- }
- }
-
- let queue1 = DispatchQueue(label: "test1",qos: .userInteractive, attributes: .concurrent)
- let queue2 = DispatchQueue(label: "test2", qos: .unspecified, attributes: .concurrent)
- queue1.async(execute: item1)
- queue2.async(execute: item2)
- 复制代码
我这边运行结果显示item1执行完了,item2才开始打印3798。
for循环次数不用太大,效果也可以很明显,您可以自己探索一下。
其实我是个标题党,我只是个菜鸟。我只是花了几天时间仔细研究了苹果开发者文档、几本教材以及十多篇帖子,从头到尾调试了多遍代码,总结并写了这样一篇文章,并取名《一天精通iOS Swift多线程(GCD)》。
要精通Swift多线程,还是要多在实践中使用,在使用过程中反复思索、反复优化,这项技术很快就会成为你的拿手好戏。
多线程虽好,但请不要滥用,不要为了炫技去用多线程,毕竟当前的CPU性能已经非常高,每秒钟可执行万亿次级别的操作,而屏幕每秒钟仅仅刷新几十、上百次,眨眼的功夫大量的代码就执行完了。在必要的地方再去用多线程吧,代码整洁、问题少、应用稳定可靠才更重要。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。