当前位置:   article > 正文

一天精通iOS Swift多线程(GCD)_swift 异步串行

swift 异步串行

一天精通iOS Swift多线程(GCD)

Grand Central Dispatch简称GCD,苹果官方推荐给开发者使用的首选多线程解决方案。多线程开发涉及的细节非常多,下面我会用例子细致的讲解GCD,请一定要精读,一定要用Xcode或Playground多次运行代码去对比结果。实践出真知,练习完这篇文章,你一定会觉得精通Swift多线程原来很简单。

本文前半部分,我会尽可能精简话语,降低入门门槛,随着理解的深入,后面我会循序渐进地讲详细一些。

第一部分:基础篇

1. 串行、并行、同步、异步

  • 串行:在本文中指串行队列,多个任务放在串行队列里执行,只能按顺序依次运行,前一个运行完成,下一个才能开始运行;前一个没运行完,后一个只能排队等着。以此类推,直到所有任务都运行完成。
  • 并行:在本文中指并行队列,多个任务放在并行队列里执行,可以同时运行。
  • 同步:在本文中指同步执行任务,是在一个线程里按顺序执行多项任务,执行结束的顺序是固定的、和任务的执行顺序相同。总耗时是所有任务耗时之和。
  • 异步:在本文中指异步执行任务,也是按顺序执行多项任务,但是是放在多个线程里同时运行,执行结束的顺序是随机的、不可预估的。总耗时大约是耗时最长的那项任务所消耗的时间。

2. DispatchWorkItem

调度工作项:其实就是一项任务,可以把你想要执行的代码写成闭包,在DispatchWorkItem初始化时传进去,方便后续管理任务,并且会让代码更整洁。
官网原文:The work you want to perform, encapsulated in a way that lets you attach a completion handle or execution dependencies.

调度工作项初始化,正常情况下,使用第一种方式即可(特殊情况后续会再讲解):

  1. //1. 只带尾随闭包
  2. let item1 = DispatchWorkItem {
  3. print("item1")
  4. }
  5. //2. 指定qos(执行优先级)或flags(特殊行为标记)
  6. let item2 = DispatchWorkItem(qos: .userInteractive, flags: .barrier) {
  7. print("item2")
  8. }
  9. 复制代码

3. DispatchQueue简介

调度队列:一个对象,用来管理任务在app的主线程或后台线程串行或并行执行。
官网原文:An object that manages the execution of tasks serially or concurrently on your app's main thread or on a background thread.

DispatchQueue有三种类型:

  • Main queue
  • Global queue
  • Custom queue

3.1 Main queue(主队列,串行)

Main queue与主线程关联的调度队列,是一种串行队列(Serial),与UI相关的操作必须放在Main queue中执行,获取方式是:

  1. let mainQueue = DispatchQueue.main
  2. 复制代码

3.2 Global queue(全局队列,并行)

Global queue运行在后台线程,是系统内共享的全局队列,是一种并行队列(Concurrent),用于处理并发任务,获取方式是:

  1. let globalQueue = DispatchQueue.global()
  2. 复制代码

3.3 Custom queue(自定义队列,默认串行)

Custom queue运行在后台线程,默认是串行队列(Serial),初始化时指定attributes参数为 .concurrent,可以创建成并行队列(Concurrent),创建方式如下:

  1. //串行队列,label名字随便取
  2. let serialQueue = DispatchQueue(label: "test")
  3. //并行队列
  4. let concurrentQueue = DispatchQueue(label: "test", attributes: .concurrent)
  5. 复制代码

4. DispatchGroup简介

调度组:一个小组,你可以把多项任务放到一个组里,方便进行统一管理(直译过来并不好理解)。
官网原文:A group of tasks that you monitor as a single unit.

DispatchGroup可以很方便的管理多项任务。比如当同一组里的所有事件都完成后,GCD API可以发送通知,执行相应的操作。常用方法:

  • notify():调度组里的所有任务执行完毕,会在此收到通知,不会阻塞当前线程。
  • wait():一直等待,直到调度组里所有任务都执行完毕或等待超时,阻塞当前线程。

第二部分:实战篇

5. 使用DispatchQueue

新建Playground项目,定义四个调度任务,提供给下文调用,可大幅降低下文代码量,部分运行结果请自己复制代码多次运行感受,我只讲结果:

  1. import Foundation
  2. //定义四个调度任务,打印当前线程数据
  3. let item1 = DispatchWorkItem {
  4. for i in 0...4{
  5. print("item1 -> \(i) thread: \(Thread.current)")
  6. }
  7. }
  8. let item2 = DispatchWorkItem {
  9. for i in 0...4{
  10. print("item2 -> \(i) thread: \(Thread.current)")
  11. }
  12. }
  13. let item3 = DispatchWorkItem {
  14. for i in 0...4{
  15. print("item3 -> \(i) thread: \(Thread.current)")
  16. }
  17. }
  18. let item4 = DispatchWorkItem {
  19. for i in 0...4{
  20. print("item4 -> \(i) thread: \(Thread.current)")
  21. }
  22. }
  23. 复制代码

5.1 异步执行

  1. //主队列追加异步任务,按顺序打印
  2. let mainQueue = DispatchQueue.main
  3. mainQueue.async(execute: item1)
  4. mainQueue.async(execute: item2)
  5. mainQueue.async(execute: item3)
  6. mainQueue.async(execute: item4)
  7. //全局队列追加异步任务,随机打印
  8. let globalQueue = DispatchQueue.global()
  9. globalQueue.async(execute: item1)
  10. globalQueue.async(execute: item2)
  11. globalQueue.async(execute: item3)
  12. globalQueue.async(execute: item4)
  13. //自定义串行队列追加异步任务,按顺序打印
  14. let serialQueue = DispatchQueue(label: "serial")
  15. serialQueue.async(execute: item1)
  16. serialQueue.async(execute: item2)
  17. serialQueue.async(execute: item3)
  18. serialQueue.async(execute: item4)
  19. //自定义并行队列追加异步任务,随机打印
  20. let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
  21. concurrentQueue.async(execute: item1)
  22. concurrentQueue.async(execute: item2)
  23. concurrentQueue.async(execute: item3)
  24. concurrentQueue.async(execute: item4)
  25. 复制代码

注:在串行队列中执行异步任务,结果跟执行同步任务完全一样

5.2 同步执行

  1. //主队列追加同步任务,会引起死锁
  2. let mainQueue = DispatchQueue.main
  3. mainQueue.sync(execute: item1)
  4. mainQueue.sync(execute: item2)
  5. mainQueue.sync(execute: item3)
  6. mainQueue.sync(execute: item4)
  7. //全局队列追加同步任务,按顺序打印
  8. let globalQueue = DispatchQueue.global()
  9. globalQueue.sync(execute: item1)
  10. globalQueue.sync(execute: item2)
  11. globalQueue.sync(execute: item3)
  12. globalQueue.sync(execute: item4)
  13. //自定义串行队列追加同步任务,按顺序打印
  14. let serialQueue = DispatchQueue(label: "serial")
  15. serialQueue.sync(execute: item1)
  16. serialQueue.sync(execute: item2)
  17. serialQueue.sync(execute: item3)
  18. serialQueue.sync(execute: item4)
  19. //自定义并行队列追加同步任务,按顺序打印
  20. let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
  21. concurrentQueue.sync(execute: item1)
  22. concurrentQueue.sync(execute: item2)
  23. concurrentQueue.sync(execute: item3)
  24. concurrentQueue.sync(execute: item4)
  25. 复制代码

注:在并行队列中执行同步任务,跟在串行队列中执行异步或同步任务,结果完全一样。 在主队列中不能混入同步任务,否则会引起死锁。

5.3 同步异步混合执行

  1. //主队列同步异步混合,会引起死锁
  2. let mainQueue = DispatchQueue.main
  3. mainQueue.sync(execute: item1)//同步任务
  4. mainQueue.async(execute: item2)
  5. mainQueue.async(execute: item3)
  6. mainQueue.async(execute: item4)
  7. //全局队列同步异步混合,同步任务按顺序打印,异步任务随机打印
  8. //本例中同步任务执行完,才会执行后续的异步任务
  9. let globalQueue = DispatchQueue.global()
  10. globalQueue.sync(execute: item1)//同步任务
  11. globalQueue.async(execute: item2)
  12. globalQueue.async(execute: item3)
  13. globalQueue.async(execute: item4)
  14. //自定义串行队列同步异步混合,按顺序打印
  15. let serialQueue = DispatchQueue(label: "serial")
  16. serialQueue.sync(execute: item1)//同步任务
  17. serialQueue.async(execute: item2)
  18. serialQueue.async(execute: item3)
  19. serialQueue.async(execute: item4)
  20. //自定义并行队列同步异步混合,同步任务按顺序打印,异步任务随机打印
  21. //本例中同步任务执行完,才会执行后续的异步任务
  22. let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
  23. concurrentQueue.sync(execute: item1)//同步任务
  24. concurrentQueue.async(execute: item2)
  25. concurrentQueue.async(execute: item3)
  26. concurrentQueue.async(execute: item4)
  27. 复制代码

注:在并行队列中执行同步任务,跟在串行队列中执行异步或同步任务,结果完全一样。 在主队列中不能混入同步任务,否则会引起死锁。

6. 死锁分析

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.

  1. //会引起死锁
  2. let mainQueue = DispatchQueue.main
  3. mainQueue.async(execute: item1)
  4. mainQueue.async(execute: item2)
  5. mainQueue.async(execute: item3)
  6. mainQueue.sync(execute: item4)//同步任务
  7. 复制代码

有人可能会想,如果A在B之后呢?是不是就不会引起死锁?看起来不会死锁,可惜Playground运行这样的代码,每次都崩溃,应该是程序刚运行,主队列就存在我们看不到的异步任务。

  1. //依然会引起死锁
  2. let mainQueue = DispatchQueue.main
  3. mainQueue.sync(execute: item1)//同步任务
  4. mainQueue.async(execute: item2)
  5. mainQueue.async(execute: item3)
  6. mainQueue.async(execute: item4)
  7. 复制代码

因此只能认为:主队列上不能存在同步任务,否则一定会引起死锁。

6.2 其他队列死锁

上文提到主队列死锁,那其他类型的队列会不会引起死锁呢?下面来试一下:

  • 自定义串行队列嵌套同步任务,会引起死锁
  1. let serialQueue = DispatchQueue(label: "serial")
  2. //死锁
  3. serialQueue.sync {
  4. print("同步执行 thread: \(Thread.current)")
  5. serialQueue.sync {
  6. print("同步执行 thread: \(Thread.current)")
  7. }
  8. }
  9. //死锁
  10. serialQueue.async {
  11. print("异步执行 thread: \(Thread.current)")
  12. serialQueue.sync {
  13. print("同步执行 thread: \(Thread.current)")
  14. }
  15. }
  16. //不会引起死锁
  17. serialQueue.sync {
  18. print("同步执行 thread: \(Thread.current)")
  19. serialQueue.async {
  20. print("异步执行 thread: \(Thread.current)")
  21. }
  22. }
  23. //不会引起死锁
  24. serialQueue.async {
  25. print("异步执行 thread: \(Thread.current)")
  26. serialQueue.async {
  27. print("异步执行 thread: \(Thread.current)")
  28. }
  29. }
  30. 复制代码
  • 并行队列嵌套同步任务,不会引起死锁
  1. //自定义并行队列(全局并行队列结果一样)
  2. let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
  3. //不会引起死锁
  4. concurrentQueue.async {
  5. print("异步执行 thread: \(Thread.current)")
  6. concurrentQueue.sync {
  7. print("同步执行 thread: \(Thread.current)")
  8. }
  9. }
  10. //不会引起死锁
  11. concurrentQueue.sync {
  12. print("同步执行 thread: \(Thread.current)")
  13. concurrentQueue.sync {
  14. print("同步执行 thread: \(Thread.current)")
  15. }
  16. }
  17. //不会引起死锁
  18. concurrentQueue.sync {
  19. print("同步执行 thread: \(Thread.current)")
  20. concurrentQueue.async {
  21. print("异步执行 thread: \(Thread.current)")
  22. }
  23. }
  24. //不会引起死锁
  25. concurrentQueue.async {
  26. print("异步执行 thread: \(Thread.current)")
  27. concurrentQueue.async {
  28. print("异步执行 thread: \(Thread.current)")
  29. }
  30. }
  31. 复制代码

6.3 死锁总结

通过上文可以看到,自定义串行队列嵌套同步任务,也是可以引起死锁的,所以死锁不是主队列的专利。但为什么会引起死锁,核心原因是什么?运行下面的代码看看结果:

  1. print("=> 开始执行")
  2. let mainQueue = DispatchQueue.main
  3. mainQueue.async(execute: item1)//异步任务
  4. print("=> 执行完毕1")
  5. let globalQueue = DispatchQueue.global()
  6. globalQueue.sync(execute: item2)//同步任务
  7. print("=> 执行完毕2")
  8. let serialQueue = DispatchQueue(label: "serial")
  9. serialQueue.sync(execute: item3)//同步任务
  10. print("=> 执行完毕3")
  11. let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
  12. concurrentQueue.sync(execute: item4)//同步任务
  13. print("=> 执行完毕all")
  14. 运行结果:
  15. => 开始执行
  16. => 执行完毕1
  17. item2 -> 0 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  18. item2 -> 1 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  19. item2 -> 2 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  20. item2 -> 3 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  21. item2 -> 4 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  22. => 执行完毕2
  23. item3 -> 0 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  24. item3 -> 1 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  25. item3 -> 2 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  26. item3 -> 3 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  27. item3 -> 4 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  28. => 执行完毕3
  29. item4 -> 0 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  30. item4 -> 1 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  31. item4 -> 2 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  32. item4 -> 3 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  33. item4 -> 4 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  34. => 执行完毕all
  35. item1 -> 0 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  36. item1 -> 1 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  37. item1 -> 2 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  38. item1 -> 3 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  39. item1 -> 4 thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
  40. 复制代码

看出什么问题没有?四组代码的运行结果完全一样,连线程信息也都一摸一样,都是运行在主线程上(main thread),并且第一组的代码放在了最后执行。也就是说:

  • 主队列上的所有任务(只有可能是异步任务)和其他队列的同步任务都运行在主线程上(主线程有且只有一个)。
  • 线程不在乎任务是同步还是异步,只有队列才在乎。
  • 线程不会死锁,只有队列才会死锁。

主队列添加同步任务会造成死锁的根本原因是:

  • 主队列只能运行在主线程(重要的事情再说一遍)。
  • 主队列没有本事开启后台线程去干别的事情。
  • 主队列一旦混入同步任务,就会跟已经存在的异步任务相互等待,导致死锁。

自定义串行队列添加同步任务不会死锁,因为:

自定义串行队列有能力启动主线程和后台线程(只能启动一个后台线程)。 自定义串行队列遇到同步任务,会自动安排在主线程执行;遇到异步任务,自动安排在后台线程执行,所以不会死锁。

并行队列添加同步任务不会死锁,因为:

并行队列有能力启动主线程和后台线程(可以启动一个或多个后台线程,部分设备上可以启动多达64个后台线程)。 并行队列遇到同步任务,会自动安排在主线程执行;遇到异步任务,自动安排在后台线程执行,所以不会死锁。

自定义串行队列一个异步或同步任务(A)嵌套另一个同步任务(B)会引起死锁,因为:

A、B任务等效为:A1 -> B -> A2,B是同步任务,B在A1之后、A2之前,B必须等A2执行完才能执行,A2必须等B执行完才能执行,A2执行完才算A执行完了,逻辑上已经陷入死循环,两者相互等待,导致死锁。所以,串行队列不能嵌套同步任务,否则会引起死锁。

7. DispatchQueue切换

7.1 背景介绍

这一章来模拟网络请求:在APP中请求网络数据(任务A: 耗时10s),获取数据后进行一定的处理(任务B: 耗时5s),最后刷新UI。

假如A和B都是同步任务,放主队列会死锁,而放其他任何队列,界面都会卡死15s,如果不信,把下面代码里的两种线程休眠方法(二选一,其实不止这两种),放在APP UIViewController里试试:

  1. override func viewDidAppear(_ animated: Bool) {
  2. //1. 全局队列执行同步任务
  3. DispatchQueue.global().sync {
  4. sleep(15)//当前线程休眠15
  5. }
  6. //2. 主队列执行异步任务
  7. DispatchQueue.main.async {
  8. sleep(15)//当前线程休眠15
  9. }
  10. }
  11. 复制代码

不出所料,两种方法,均让界面卡死15s。回想一下上文说过的:所有的同步任务最终都要安排到主线程运行,主线程运行长耗时任务都会导致界面严重卡顿,所以:

能异步执行的长耗时任务,千万不要同步执行。 长耗时同步任务欠下的债,都由界面来偿还。

假如A和B都是异步任务,即使这样,你也不能都放在主队列中处理,这样也会导致APP界面卡住15s,因为上面说到了:主线程运行长耗时任务都会导致界面严重卡顿。

所有的长耗时任务,千万不要放在主队列中执行。 主队列长耗时异步任务欠下的债,也都由界面来偿还。

说了那么多,你现在应该能够深切地理解各种队列的运行原理了。

7.2 网络请求实例

现在讲讲使用GCD多线程处理网络请求的正确做法:A、B都定义成异步任务,在并行队列中嵌套异步任务,最后切换到主队列去刷新UI,这样做界面可以保证最流畅。

  1. //创建并行队列,尽量用自定义队列,免得自己的代码质量不过关,影响全局队列
  2. let queue = DispatchQueue(label: "com.apple.request", attributes: .concurrent)
  3. //异步执行
  4. queue.async {
  5. print("开始请求数据 \(Date()) thread: \(Thread.current)")
  6. sleep(10)//模拟网络请求
  7. print("数据请求完成 \(Date()) thread: \(Thread.current)")
  8. //异步执行
  9. queue.async {
  10. print("开始处理数据 \(Date()) thread: \(Thread.current)")
  11. sleep(5)//模拟数据处理
  12. print("数据处理完成 \(Date()) thread: \(Thread.current)")
  13. //切换到主队列,刷新UI
  14. DispatchQueue.main.async {
  15. print("UI刷新成功 \(Date()) thread: \(Thread.current)")
  16. }
  17. }
  18. }
  19. //运行结果
  20. 开始请求数据 2020-08-06 06:40:57 +0000 thread: <NSThread: 0x7ff917d8c0c0>{number = 4, name = (null)}
  21. 数据请求完成 2020-08-06 06:41:07 +0000 thread: <NSThread: 0x7ff917d8c0c0>{number = 4, name = (null)}
  22. 开始处理数据 2020-08-06 06:41:07 +0000 thread: <NSThread: 0x7ff8f7d0c190>{number = 3, name = (null)}
  23. 数据处理完成 2020-08-06 06:41:12 +0000 thread: <NSThread: 0x7ff8f7d0c190>{number = 3, name = (null)}
  24. UI刷新成功 2020-08-06 06:41:12 +0000 thread: <NSThread: 0x7ff917c0e7e0>{number = 1, name = main}
  25. 复制代码

可以看到队列和线程均进行了预期的切换,GCD队列切换像俄罗斯套娃一样,一层一层的嵌套就行,等嵌套出问题了,去第6章死锁分析寻找原因进行修改即可。

8. 使用DispatchGroup

如果希望多项任务执行完毕后,再去执行另一项任务,可以使用DispatchGroup。这些任务可以放在同一队列中,也可以放在不同队列中。

DispatchGroup常用的方法:

group.wait():阻塞当前线程,一直到group所有任务执行完毕。

group.notify():所有任务执行完毕后,异步发送通知,不阻塞当前线程。

8.1 使用group.notify()改写一下上一章网络请求的例子:

  1. let group = DispatchGroup()
  2. let queue = DispatchQueue(label: "com.apple.request", attributes: .concurrent)
  3. //异步执行
  4. queue.async(group: group) {
  5. print("开始请求数据 \(Date()) thread: \(Thread.current)")
  6. sleep(10)//模拟网络请求
  7. print("数据请求完成 \(Date()) thread: \(Thread.current)")
  8. //异步执行
  9. queue.async(group: group) {
  10. print("开始处理数据 \(Date()) thread: \(Thread.current)")
  11. sleep(5)//模拟数据处理
  12. print("数据处理完成 \(Date()) thread: \(Thread.current)")
  13. }
  14. }
  15. print("开始监听")
  16. //在当前队列监听
  17. group.notify(queue: queue) {
  18. //切换到主队列,刷新UI
  19. DispatchQueue.main.async {
  20. print("UI刷新成功 \(Date()) thread: \(Thread.current)")
  21. }
  22. }
  23. print("监听完毕")
  24. //运行结果
  25. 开始监听
  26. 监听完毕
  27. 开始请求数据 2020-08-06 06:45:22 +0000 thread: <NSThread: 0x7fe312f30b60>{number = 4, name = (null)}
  28. 数据请求完成 2020-08-06 06:45:32 +0000 thread: <NSThread: 0x7fe312f30b60>{number = 4, name = (null)}
  29. 开始处理数据 2020-08-06 06:45:32 +0000 thread: <NSThread: 0x7fe312e70d70>{number = 5, name = (null)}
  30. 数据处理完成 2020-08-06 06:45:37 +0000 thread: <NSThread: 0x7fe312e70d70>{number = 5, name = (null)}
  31. UI刷新成功 2020-08-06 06:45:37 +0000 thread: <NSThread: 0x7fe312c0e7e0>{number = 1, name = main}
  32. 复制代码

如你所愿,运行结果跟上文一致。

8.2 精简代码,直接在主队列监听通知、刷新UI:

  1. let group = DispatchGroup()
  2. let queue = DispatchQueue(label: "com.apple.request", attributes: .concurrent)
  3. //异步执行
  4. queue.async(group: group) {
  5. print("开始请求数据 \(Date()) thread: \(Thread.current)")
  6. sleep(10)//模拟网络请求
  7. print("数据请求完成 \(Date()) thread: \(Thread.current)")
  8. //异步执行
  9. queue.async(group: group) {
  10. print("开始处理数据 \(Date()) thread: \(Thread.current)")
  11. sleep(5)//模拟数据处理
  12. print("数据处理完成 \(Date()) thread: \(Thread.current)")
  13. }
  14. }
  15. print("开始监听")
  16. //切换到主队列监听,刷新UI
  17. group.notify(queue: DispatchQueue.main) {
  18. print("UI刷新成功 \(Date()) thread: \(Thread.current)")
  19. }
  20. print("监听完毕")
  21. //运行结果
  22. 开始监听
  23. 监听完毕
  24. 开始请求数据 2020-08-06 06:49:31 +0000 thread: <NSThread: 0x7fc608c80370>{number = 4, name = (null)}
  25. 数据请求完成 2020-08-06 06:49:41 +0000 thread: <NSThread: 0x7fc608c80370>{number = 4, name = (null)}
  26. 开始处理数据 2020-08-06 06:49:41 +0000 thread: <NSThread: 0x7fc608d2b200>{number = 5, name = (null)}
  27. 数据处理完成 2020-08-06 06:49:46 +0000 thread: <NSThread: 0x7fc608d2b200>{number = 5, name = (null)}
  28. UI刷新成功 2020-08-06 06:49:46 +0000 thread: <NSThread: 0x7fc608c0e7e0>{number = 1, name = main}
  29. 复制代码

如你所愿,运行结果依然一致。

8.3 使用group.wait()改写:

  1. let group = DispatchGroup()
  2. let queue = DispatchQueue(label: "com.apple.request", attributes: .concurrent)
  3. //异步执行
  4. queue.async(group: group) {
  5. print("开始请求数据 \(Date()) thread: \(Thread.current)")
  6. sleep(10)//模拟网络请求
  7. print("数据请求完成 \(Date()) thread: \(Thread.current)")
  8. //异步执行
  9. queue.async(group: group) {
  10. print("开始处理数据 \(Date()) thread: \(Thread.current)")
  11. sleep(5)//模拟数据处理
  12. print("数据处理完成 \(Date()) thread: \(Thread.current)")
  13. }
  14. }
  15. print("开始监听")
  16. //切换到主队列监听,刷新UI
  17. group.notify(queue: DispatchQueue.main) {
  18. print("UI刷新成功 \(Date()) thread: \(Thread.current)")
  19. }
  20. group.wait()//阻塞当前线程
  21. print("监听完毕")
  22. //运行结果
  23. 开始监听
  24. 开始请求数据 2020-08-06 06:53:00 +0000 thread: <NSThread: 0x7fe1ad538580>{number = 4, name = (null)}
  25. 数据请求完成 2020-08-06 06:53:10 +0000 thread: <NSThread: 0x7fe1ad538580>{number = 4, name = (null)}
  26. 开始处理数据 2020-08-06 06:53:10 +0000 thread: <NSThread: 0x7fe1b8010060>{number = 5, name = (null)}
  27. 数据处理完成 2020-08-06 06:53:15 +0000 thread: <NSThread: 0x7fe1b8010060>{number = 5, name = (null)}
  28. 监听完毕
  29. UI刷新成功 2020-08-06 06:53:15 +0000 thread: <NSThread: 0x7fe1ad40e7e0>{number = 1, name = main}
  30. 复制代码

可以看到group.wait()的确阻塞了当前线程。

第三部分:进阶篇

9. DispatchGroup挂起、恢复

在第7章的例子里,嵌套了三层,还不算多,但是已经可以隐约感受到嵌套地狱了。这一节用队列挂起、恢复重写,解决嵌套问题。以后遇到更多层级的嵌套,可以用同样的方法解决。

  1. let group = DispatchGroup()
  2. let queue1 = DispatchQueue(label: "com.apple.request", attributes: .concurrent)
  3. let queue2 = DispatchQueue(label: "com.apple.response", attributes: .concurrent)
  4. queue2.suspend()//队列挂起
  5. //异步执行
  6. queue1.async(group: group) {
  7. print("开始请求数据 \(Date()) thread: \(Thread.current)")
  8. sleep(10)//模拟网络请求
  9. print("数据请求完成 \(Date()) thread: \(Thread.current)")
  10. queue2.resume()//网络数据请求完成,恢复队列,进行数据处理
  11. }
  12. //异步执行
  13. queue2.async(group: group) {
  14. print("开始处理数据 \(Date()) thread: \(Thread.current)")
  15. sleep(5)//模拟数据处理
  16. print("数据处理完成 \(Date()) thread: \(Thread.current)")
  17. }
  18. print("开始监听")
  19. //切换到主队列监听,刷新UI
  20. group.notify(queue: DispatchQueue.main) {
  21. print("UI刷新成功 \(Date()) thread: \(Thread.current)")
  22. }
  23. print("监听完毕")
  24. 复制代码

10. 线程安全

如果有一个变量有可能被多个线程同时读写,结果便不可预期,必须进行特殊处理,来保证线程安全。

10.1 通过barrier标识设置屏障

自定义队列支持DispatchWorkItem设置flags为.barrier,可以支持barrier之前的任务全部执行完毕后,再执行.barrier任务,最后再执行.barrier之后的任务,这样处理可以保证线程安全。(注:全局队列,flags设置.barrier无效)

  1. import Foundation
  2. let item1 = DispatchWorkItem {
  3. for i in 0...4{
  4. print("item1 -> \(i) thread: \(Thread.current)")
  5. }
  6. }
  7. let item2 = DispatchWorkItem {
  8. for i in 0...4{
  9. print("item2 -> \(i) thread: \(Thread.current)")
  10. }
  11. }
  12. //给item3任务加barrier标识
  13. let item3 = DispatchWorkItem(flags: .barrier) {
  14. for i in 0...4{
  15. print("item3 barrier -> \(i) thread: \(Thread.current)")
  16. }
  17. }
  18. let item4 = DispatchWorkItem {
  19. for i in 0...4{
  20. print("item4 -> \(i) thread: \(Thread.current)")
  21. }
  22. }
  23. let item5 = DispatchWorkItem {
  24. for i in 0...4{
  25. print("item5 -> \(i) thread: \(Thread.current)")
  26. }
  27. }
  28. let queue = DispatchQueue(label: "test", attributes: .concurrent)
  29. queue.async(execute: item1)
  30. queue.async(execute: item2)
  31. queue.async(execute: item3)
  32. queue.async(execute: item4)
  33. queue.async(execute: item5)
  34. //运行结果
  35. item1 -> 0 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
  36. item2 -> 0 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
  37. item1 -> 1 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
  38. item2 -> 1 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
  39. item1 -> 2 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
  40. item2 -> 2 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
  41. item2 -> 3 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
  42. item1 -> 3 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
  43. item2 -> 4 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
  44. item1 -> 4 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
  45. item3 barrier -> 0 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
  46. item3 barrier -> 1 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
  47. item3 barrier -> 2 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
  48. item3 barrier -> 3 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
  49. item3 barrier -> 4 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
  50. item4 -> 0 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
  51. item5 -> 0 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
  52. item4 -> 1 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
  53. item4 -> 2 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
  54. item5 -> 1 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
  55. item4 -> 3 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
  56. item4 -> 4 thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
  57. item5 -> 2 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
  58. item5 -> 3 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
  59. item5 -> 4 thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
  60. 复制代码

10.2 使用DispatchSemaphore给线程上锁

DispatchSemaphore被很多人翻译成信号量,说实话我这辈子第一次听说信号量,信号还有量?什么量?多少量?
吐槽完毕,为了方便理解,在这里我把它临时翻译成红绿灯吧。
DispatchSemaphore初始化时只有一个参数value(通行数量),表示还可以通行几辆车(还可以执行几个异步任务)。
DispatchSemaphore有两个方法:

  • wait():执行一次,通行数量减1,通行数量为0时就表示红灯,全都得等着
  • signal():执行一次,通行数量加1

10.2.1 举一个99乘法表的例子,感受下DispatchSemaphore:

  1. let semaphore = DispatchSemaphore(value: 1)
  2. let queue = DispatchQueue(label: "concurrent", attributes: .concurrent)
  3. //执行9个异步任务
  4. for i in 1...9 {
  5. queue.async {
  6. semaphore.wait()//绿灯时间减1,此处变为0,红灯,全都得等着
  7. var str = ""
  8. for j in 1...9{
  9. //格式化一下字符串,后面加两个空格。如果只有个位数的,前面补个空格
  10. let value = i * j
  11. let tempStr = value <= 9 ? " \(value) " : "\(value) "
  12. str += tempStr
  13. }
  14. print(str)
  15. semaphore.signal()//绿灯时间加1,后面可继续通行
  16. }
  17. }
  18. //运行结果
  19. 1 2 3 4 5 6 7 8 9
  20. 2 4 6 8 10 12 14 16 18
  21. 3 6 9 12 15 18 21 24 27
  22. 4 8 12 16 20 24 28 32 36
  23. 5 10 15 20 25 30 35 40 45
  24. 6 12 18 24 30 36 42 48 54
  25. 7 14 21 28 35 42 49 56 63
  26. 8 16 24 32 40 48 56 64 72
  27. 9 18 27 36 45 54 63 72 81
  28. 复制代码

99乘法表显示理想

10.2.2 注释掉semaphore.wait()和semaphore.signal(),多运行几次试试看:

  1. let semaphore = DispatchSemaphore(value: 1)
  2. let queue = DispatchQueue(label: "concurrent", attributes: .concurrent)
  3. //执行9个异步任务
  4. for i in 1...9 {
  5. queue.async {
  6. //semaphore.wait()//绿灯时间减1,此处变为0,红灯,全都得等着
  7. var str = ""
  8. for j in 1...9{
  9. //格式化一下字符串,后面加两个空格。如果只有个位数的,前面补个空格
  10. let value = i * j
  11. let tempStr = value <= 9 ? " \(value) " : "\(value) "
  12. str += tempStr
  13. }
  14. print(str)
  15. //semaphore.signal()//绿灯时间加1,后面可继续通行
  16. }
  17. }
  18. //运行结果
  19. 5 10 15 20 25 30 35 40 45
  20. 4 8 12 16 20 24 28 32 36
  21. 3 6 9 12 15 18 21 24 27
  22. 1 2 3 4 5 6 7 8 9
  23. 8 16 24 32 40 48 56 64 72
  24. 9 18 27 36 45 54 63 72 81
  25. 2 4 6 8 10 12 14 16 18
  26. 6 12 18 24 30 36 42 48 54
  27. 7 14 21 28 35 42 49 56 63
  28. 复制代码

99乘法表已经失控

为了更深刻的理解,试试把上面的例1中DispatchSemaphore初始化时value设为2或3,多次运行下程序看看结果,你能感受到通行数量对失控程度的影响。

10.3 使用串行队列+计算属性,修改变量

  1. import Foundation
  2. let queue = DispatchQueue(label: "test")
  3. var a:Int = 10
  4. var b:Int{
  5. get{
  6. queue.sync {
  7. print("同步读取 thread = \(Thread.current)")
  8. return a
  9. }
  10. }
  11. set{
  12. queue.sync {
  13. print("同步写入 thread = \(Thread.current)")
  14. a = newValue
  15. }
  16. }
  17. }
  18. b = 30//赋值
  19. print("a = \(a) b = \(b) thread = \(Thread.current)")
  20. //运行结果
  21. 同步写入 thread = <NSThread: 0x7f8018c0e7e0>{number = 1, name = main}
  22. 同步读取 thread = <NSThread: 0x7f8018c0e7e0>{number = 1, name = main}
  23. a = 30 b = 30 thread = <NSThread: 0x7f8018c0e7e0>{number = 1, name = main}
  24. 复制代码

尝试修改set为异步写入,思索下结果。

11. DispatchQoS

DispatchQoS调度优先级:直译过来就是应用在任务上的服务质量或执行优先级,可以理解为任务的身份、等级。可以用来修饰DispatchWorkItem、DispatchQueue。

就像航空公司有身份的客户,在VIP休息室等飞机、坐头等舱、高质量空姐贴心服务等等,最好的服务优先都给你;如果你没有身份、只有身份证,平平安安的到达目的地就可以知足了;如果你连身份证也没有,那就去坐公交车吧。
官网原文:The quality of service, or the execution priority, to apply to tasks.

DispatchQoS有以下几种类型:

  • userInteractive: 与用户交互相关的任务,要最重视,优先处理,保证界面最流畅
  • userInitiated: 用户主动发起的任务,要比较重视
  • default: 默认任务,正常处理即可
  • utility: 用户没有主动关注的任务
  • background: 不太重要的维护、清理等任务,有空能处理完就行
  • unspecified: 别说身份了,连身份证都没有,能处理就处理,不能处理也无所谓的

DispatchQoS其实只是一个简单的优先级标识,为何会放在进阶篇里说呢?
因为对于绝大部分开发者来说,没必要设置这个标识,设置了也只是徒增代码复杂度,花里胡哨的技巧用了一大堆,代码量不小,最后到处都是bug,有意义吗?
还是尽量让代码简单点、少出问题最好,很多书里都讲:代码越少,bug越少。当有一天你想增强用户体验、提高代码运行效率、优化设备能耗,说明你的应用质量、代码档次都已经很不错了,明显属于进阶水准,这时你应该去试试这个标识了。所以,鄙人认为,DispatchQoS属于进阶内容。

11.1 在DispatchWorkItem上添加DispatchQoS标识:

  1. import Foundation
  2. let item1 = DispatchWorkItem(qos: .userInteractive) {
  3. for i in 0...9999{
  4. print("--item1 -> \(i) thread: \(Thread.current)")
  5. }
  6. }
  7. let item2 = DispatchWorkItem(qos: .unspecified) {
  8. for i in 0...9999{
  9. print("item2 -> \(i) thread: \(Thread.current)")
  10. }
  11. }
  12. let queue = DispatchQueue(label: "test1", attributes: .concurrent)
  13. queue.async(execute: item1)
  14. queue.async(execute: item2)
  15. 复制代码

运行结果显示item1执行完了,item2才开始打印3824。
for循环次数需要调大一些,否则效果不明显。

11.2 在DispatchQueue上添加DispatchQoS标识:

  1. import Foundation
  2. let item1 = DispatchWorkItem {
  3. for i in 0...9999{
  4. print("--item1 -> \(i) thread: \(Thread.current)")
  5. }
  6. }
  7. let item2 = DispatchWorkItem {
  8. for i in 0...9999{
  9. print("item2 -> \(i) thread: \(Thread.current)")
  10. }
  11. }
  12. let queue1 = DispatchQueue(label: "test1",qos: .userInteractive, attributes: .concurrent)
  13. let queue2 = DispatchQueue(label: "test2", qos: .unspecified, attributes: .concurrent)
  14. queue1.async(execute: item1)
  15. queue2.async(execute: item2)
  16. 复制代码

我这边运行结果显示item1执行完了,item2才开始打印3798。
for循环次数不用太大,效果也可以很明显,您可以自己探索一下。

结束语

其实我是个标题党,我只是个菜鸟。我只是花了几天时间仔细研究了苹果开发者文档、几本教材以及十多篇帖子,从头到尾调试了多遍代码,总结并写了这样一篇文章,并取名《一天精通iOS Swift多线程(GCD)》。
要精通Swift多线程,还是要多在实践中使用,在使用过程中反复思索、反复优化,这项技术很快就会成为你的拿手好戏。
多线程虽好,但请不要滥用,不要为了炫技去用多线程,毕竟当前的CPU性能已经非常高,每秒钟可执行万亿次级别的操作,而屏幕每秒钟仅仅刷新几十、上百次,眨眼的功夫大量的代码就执行完了。在必要的地方再去用多线程吧,代码整洁、问题少、应用稳定可靠才更重要。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/691541
推荐阅读
  

闽ICP备14008679号