大多数浏览器和
Developer App 均支持流媒体播放。
-
通过 Swift 3 中的 GCD 进行并发编程
Swift 3 提供了新的接口,让开发者能够更加轻松地使用 Grand Central Dispatch 编写 app。了解 GCD 的基础知识,以及用它来架构 Swift app 的最佳做法。您还将了解新的 API 和其他 GCD 改进。
资源
相关视频
WWDC21
WWDC16
-
下载
Swift 3中使用GCD并发编程
下午好
今天下午 我将要和大家聊一下 如何结构化你的程序 使用并发编程 以及我们在这一年中做了哪些东西 在Swift 3的GCD的新特性 我叫Matt 稍后Pierre会和我一起 我们都在Apple达尔文运行时团队
当你创建一个新的工程 你会得到一些类似于这个的东西 你们都有应用 那个应用有它的主线程
这个主线程用来 运行所有生成用户界面的代码
当你开始往应用中添加代码的时候 你会发现应用的性能 会大幅度地变化 比如 你开始引入很多工作 像数据转换或图片处理 在你的主线程上 你会发现你的用户界面变糟糕很多 在macOS上 这可能会是出现不停转动的圆圈 在iOS上 这可能是一些更微妙的东西 你的用户界面会慢下来 或者甚至是完全停了
我会带大家看一些基本的内容 关于如何结构还你的应用 以此避免这类问题 稍后 Pierre会上台 给大家谈一些更高级的话题
那么我们应该如何处理这类问题呢?
我们要从介绍程序的多线程概念开始 多线程允许应用中的多个部分 在同一时刻运行 在我们的系统中 你通过创建线程来实现多线程 一个中央处理器核心 可以在任意时刻处理一个线程 但是引入多线程的好处 引入多线程的坏处 是它会使得很难维护线程的安全性 你新引入的其它线程 可以观察破坏代码不变量的效果 当你在其它线程执行操作的时候
这变成有点问题了
那我们如何处理呢? GCD是我们平台上的多线程库 它帮你写多线程代码 工作于所有平台 从Apple Watch 到所有的iOS设备 Apple TV以及Mac
所以为了帮助你 使用多线程 我们在线程上引进了一些抽象
那是调度队列和运行循环 调度队列是是一个结构 允许你提交 工作项目给队列 在Swift中 这是闭包
调度会带来一个线程和一个服务 当调度结束在那个线程上 所有工作的运行 它会拆毁工作线程 正如我前面说过的 你也可以创建你自己的线程 在那些线程上 你可以运行运行循环 最后 在第一张幻灯片 你们有了主线程 它是特殊的 它同时有主运行循环和主队列
所以调度队列有两个主要方法 你可以给它们提交工作 第一个是异步执行 你可以给多个工作项目进行排队 给你的调度队列 然后调度 会引入一个线程来执行那个工作
调度会一个个从队列中 拿到项目并执行它们
当它完成了队列中的所有项目 系统会回收那个线程
第二个执行模式是异步执行
这是 比如 如果我们的设置和前面的一下 调度队列有一些异步的工作 但是你有你自己的线程 那个线程想要在队列上运行代码 等待它的发生 你可以向调度队列提交那个工作 然后它会在那里被卡住 它会一直等待 直到你要执行的那个项目完成之后 我们可能给那个队列增加更多异步工作 然后调度会带来一个线程 为了服务那个队列中的项目 同样 异步项目会被执行 当运行你要运行的同步项目 调度队列会对取消那个等待线程的控制 执行那个项目 调度队列的控制会返回给一个工作线程 通过调度来控制 它会继续排出那个队列中的其它项目 然后回收使用过的线程 那么现在我已告诉你 如何向调度提交工作 那么我们如何使用它 帮助我们解决我们前面遇到的问题呢?
我们要做的是把工作从你的主线程拿开 那会阻碍用户界面 我们通过拿开主线程的转化来实现它 把它放在不同的队列中运行
所有你可以拿掉转化 把它放回到一个调度队列 当你想要转化数据 你可以把那个数据的数值转移到 你在另外队列的转化代码中 转化它 然后返回给你的主线程 这就允许你执行那个工作 当主线程空闲和服务事件
那么在真实代码中 这是什么样呢?
好吧 这是非常简单的 首先 你可以创建调度队列 通过创建一个 DispatchQueue对象来提交你的工作 它有一个标签 那个标签在调试器中是可见的 当你写应用的时候
调度队列执行你提交的工作 安装先进先出的顺序 也就是 你提交给队列的顺序 就是调度运行它们的顺序
然后你可以在调度队列中使用异步方法 来给队列提交工作
所以既然我们已经提交了 我们的调整大小操作 给一个不同的队列 那我们如何把它返回给主线程呢 那也是非常简单的 调度主队列 服务于所有执行在主线程上的项目
这意味着你可以只调用 DispatchQueue main方法 然后在那个主队列中调用 async方法 那样代码会执行 你可以更新你的用户界面
正如你所看到的那样 链接工作是非常简单的 从一个队列到另一个队列 和返回到你的主队列 那么我已经看到了如何控制你的代码 和将它放在不同的线程
这是要有一些代价的
你必须控制你的应用中的一个并发 调度使用的线程池
会限制你能得到的并发数 为了使用设备上所有的调用
然而 当你阻碍那些线程的时候 如果你等待你的程序中的另外部分 或者你等待系统调用
那些被阻碍的工作线程 会导致更多的工作线程 调度是尝试给你你应得的并发数 通过给你一个新的线程来继续执政代码 这意味着选择正确的调度队列数量 是非常重要的 来执行代码 否则的话 你可能会阻碍一个线程 另外一个线程会产生 然后你阻碍了另一个 以此类推 这个模式就是我们称为线程爆炸的东西 我们在去年的演讲中 讲了线程爆炸和它的问题 使用GCD建立响应式和高效应用 所以我建议大家回去看看 去年的那个演讲
现在我们已看到了 如何做这个简单的事情 就是把工作从主线程搬到其它队列中 但是我们如何真的把 这个应用于你的应用呢?
如果我们回到前面的系统 你想要做的就是确定你的应用的面积 使用独立的数据流 正如我们看到过的那样 这可能是图像转化 或者你可能有一个数据库
你想要计算那些面积 把它们分割成独立的子系统 然后你想要把那些每一个子系统 返回给一个调度队列 这会给每一个子系统一个队列 来独立地执行工作 这样就不会有太多队列和进程的问题 我们在前几张幻灯片看到了 把工作串联在一起是很简单的 你可以异步处理一个一个的模块 然后到另一个队列 然后回到主队列 但是我还想给大家展示第二种模式 也是同样的有用 那就是组合工作和等待工作结束
如果你有单独的一件事 想要产生多个不同的工作项目 而你想往前进 如果那些工作项目 当那些工作项目完成后 你可以做那个
为了实现它 调度可以帮助你 所以如果我们回到刚才的示意图 如果用户界面产生三个不同的工作条目
你可以创建一个调度组
调度组是帮助你追踪工作的
它们在Swift中是非常容易创建的 你只要创建一个 DispatchGroup对象
现在当你向调度提交工作 你可以向你的异步调用增加这个组 作为一个可选的参数
你可以向那个组添加更多的工作 你可以向不同的队列添加 但是用同一个组联系起来
每次你向组提交工作 它都会增加需要完成的项目的数量
最后 当你提交了所有的工作 你可以让组在所有工作被完成时通知你 你可让它在一个你选择的队列中这么做
现在一个接着一个 这些项目会开始执行
当它们执行的时候 组中的数量 会在每一个工作项目完成的时候减少 最终 当最后一个工作项目完成的时候 组就会继续 然后提交你的通知阻碍 给你请求的队列
这样 我们返回组 阻碍给主进程 它会在主线程上运行
现在还有第三个模式 我觉得我们需要展示的
前面说的两个是异步执行 第三个是关于处理同步执行
你可以使用同步执行 来帮助你序列化子系统间的状态
串行队列、调度队列 是本质的序列 你可以使用它 为了它的相互排斥属性 那就是 当你向那个队列同步地提交工作 你知道运行在那个队列中的 子系统的工作 不是同时运行的
你可以利用它来建立非常简单的路径 从其它地方的子系统中读取属性 比如 这里你可以调用队列同步 你可以返回一个值到队列同步的外面 我们会在队列中捕捉到那个值 然后在工作项目结束时返回给你
但是 当你引入这个模式的时候 你必须十分小心 因为你开始在你的子系统之间 引入一个锁顺序图
那是什么意思呢? 如果你有我们曾经有过的子系统 而且你从一个地方同步到另一个地方 然后又同步到另一个地方 最后 你同步回第一个 好了 现我们有了一个死锁
这个演讲的后面部分 Pierre会上来谈一下死锁
所以现在我们看到了 如何结构化调度使用 在你的应用中 我们如何将其应用于 你的子系统中的使用 你可以使用调度 来对你提交的工作进行分类 为了实现它 我们需要引入很多服务类
这些类用来提供一个明确的分类 你提交给调度的工作
所以它允许你作为一个开发者 来表示你提交给调度的代码的意图 调度可使用它来影响它执行代码的方式 你给我们的 代码可以不同中央处理器优先级 进行执行 不同的输入输出安排优先级 我们在去年同一个演讲中 详细地讲了QoS 使用GCD创建响应式和高效的应用 那么我们如何使用QoS类呢?
这和以前一样简单 你可以将QoS类 作为一个可选参数传给异步 我们给队列提交后台工作
如果你等会儿出现和 在一个更高的QoS上提交队列 调度会帮你解决创建的优先级颠倒 它会在你的工作之前提升项目 在调度队列 给更高的QoS
那样它们更快地执行 让你的项目 以你期待的速度执行 然而 对这一点需要重点指出的是 它不会帮助你的工作跳行 它所做的一切就是 在你之前提升所有的工作 所以你一提交工作 它就会执行
你也可以创建调度队列 它有特定的QoS类 这是非常有帮助的 比如 如果你想要始终在后台执行的后台工作 你可以创建一个队列 在后台执行所有 当你向那个队列提交工作的时候 那就是我们得到的QoS
所以在更微观的级别上 当你于一个调度队列进行同步 它获取执行内容 在你同步的那一刻 执行环境意味着类似于QoS的东西 它也意味着你现在拥有的登入环境
但是如果你想要更多的控制 你可以使用 DispatchWorkItem来创建项目 你对它们如何执行有更多的控制
比如 我们使用assignCurrentContext 创建一个工作项目 它使用执行内容的QoS 在你创建工作项目的时候 而不是你向调度队列提交它的时候
这意味着你可以创建那个项目 保存它 当你最后真的执行它 我们会想调度提交它 并带着你创建时的属性 当我们谈论工作项目的时候 DispatchWorkItem 有另外非常有用的部分 那就是等待它们的完成 你可以使用 DispatchWorkItem wait方法 来向调度表示你需要那个工作项目完成 在你继续之前
调度会响应 通过提升它前面工作的优先级 到那个QoS 就像 通过优先级翻转所做的
它可以这么做是因为 DispatchWorkItem 知道它在那里被提交 你想要在那个线程运行它 因为 调度知道它要提升那个队列 使得完成你的工作条目
需要重点指出的是 因为等待旗语和小组 不会存储这个所有者信息 这意味着如果你等待一个旗语 它不会在你的旗语信号前产生东西 来更快地执行 现在我要邀请Pierre来到台上 他会给大家讲一下同步
谢谢Matt
和Matt一起 我们看到了如何从应用的角度使用调度 我会和大家讲一下它的具体意义 从你们的对象的角度
首先 关于Swift的一点
同步不是Swift 3语言的一部分
你今天只有这个语言的一个重点 那就是你的全局变量是被 原子化地初始化的
但你没有的是你的类属性不是原子化的 你的类中的惰性属性也不是原子化的 那意味着如果你调用这些属性 而它们同时在两个环境中被初始化 你的惰性初始化器 可能实际上运行了两次
所有你必须同步
这个语言现在不会给我们很多的工具 但是那不意味着竞争不是一个问题
不存在良性竞争这种事情
对你来说 那意味着 如果你忘记了同步这一点 你会得到奔溃 或者破坏应用中用户数据
我建议大家看一下本周稍早时候的演讲 关于T San的 那是个消毒剂 是一个工具 帮助你找出 在程序中你缺少了哪些正确的异步处理
那我们拿什么来做同步处理呢? 传统上你会使用一个锁 在Swift里 因为你有一整个达尔文模块在部署中 你实际上会看到这个结构 是基于传统的C语言锁 然而 Swift假设所有被训练东西 都是可以被移动的 那不会和一个互斥体 或者一个锁一起工作 所有我们非常不建议 你在Swift中使用这一类的锁
你若你想要一个传统的锁 你可以使用的是 Foundation.Lock 因为不同于传统的基于C语言锁的结构 它是一个类 它不容易产生我前面提到过的问题
然而 那意味着 你确定了你的下一个对象 对你来说 它可能会不是所想要的 如果你想要一些小一点的东西 那看起来就像是你在C语言中的锁 然后你必须引Objective-C 引入Objective-C中的基本类 把你的锁作为一个不变量
然后你会暴露一个锁和解锁方法 一个尝试锁 如果你也需要它 你可以从Swift中调用 当你将该类分成子类
你会在那张幻灯片中 发现我们使用不公平的锁 这是一个新的API 我们在这些更新中引入的 它不易于盗版入侵 它不会旋转 不像我们复制的旋转锁 这是真实生活中最重要的
这是一个GCD的演讲 所有我们鼓励大家 使用调度队列 为了同步处理
要这么做的的第一个原因 是这些非常容易被误用 相比于传统的锁 你的代码会在可观测的方式下运行 这意味着你不能忘记解锁
另外一件事是队列事实上更好地集成到 Xcode中调试工具中的运行时
所以我们如何使用队列来同步呢
我会和大家一起讲一下 实现原子化属性的问题
这里我们有这个对象 它有一个内部的状态 我们想要以一种安全方式来访问它 我们会使用一个队列来同步
我们如何写我们的getter 和setter呢?
getter是关于返回它的内部状态 它给我们相互的排斥 Matt前面已经讲过了 setter也是那么简单 你只要设置你的新状态 和同步的其他保护和你的队列
这个模式是非常简单的 你实际上可以拓展它 对于各种更加复杂的产品
我告诉过你队列更好地 与你的调试工具整合在一起 它们也有更多的功能 在这个更新中的新东西 我们让你表达预设条件
它让你表达你在代码中有不变量 非常需要在指定的队列中运行 你以这种方式拥有
调度预设条件 你在那个队列上
有时对面是非常有用的 你想保证代码中特定片段 永远不会在那个队列中运行 因为你知道你可能与那个队列同步 你表以这种方式示这个 一个预设条件你不在那个队列中
所以那就是关于同步处理 同步你的状态
正如Matt前面所说的 这会更加好 如果你仅组织你的应用 以你的传输值不需要同步被挂起的方式 然而 在真实的代码中 你需要一些对象来访问 从简单明显的子系统
那意味着所有这些子系统 有一个引用在这些对象中 摆脱它们会是一个挑战
我现在会带你们看一下 一个四步状态任务 那会帮你得到这个高度 不会产生奇怪的难以重现的奔溃
你的状态机开始于第一件事 设置 设置是关于创建你的对象 给它你需要的属性 为了它的目的
第二 你会想要激活这个对象 那意味着你实际上让这个对象 对于其它子系统是可知的 你开始使用它以一种更加同步的世界 在性能任务方面
然后开始了难的部分 你想要摆脱那个对象 所有第三步是作废
作废是关于确保所有部分 所有你的子系统知道这个对象会消失 所以 第四 它被取消分配 那么让我们让它保持那样 设置 激活 作废 取消分配
这是非常抽象的 所以我们会讲一下 一个更加具体的例子 我希望你会关联上 让我们回到Matt前面介绍的应用 关注于两个子系统 第一 我们有我们的用户界面 它会处理事物类似于应用中的标题栏
我会假设你可以观察一些状态改变 在你的子系统中 所以那样 举个例子 对于我们的数据转换子系统 当它开始执行一些工作 我们给用户展示了一个视觉上的迹象 当数据转化子系统停止做任何工作时 视觉迹象消失了
所以我们如何实现 BusyController呢?
所以我们记得第一步是设置 设置是关于为你的代码挑选属性 和动画之类的 这都取决于你
然后我们想要开始使用那个对象 那是激活 我们激活它 那对我们的控制器意味着什么呢 那意味着我们想要开始接受 这些状态通知 状态变化的通知 所以我们会向子系统注册 并要求在主队列上接受通知 我们想在用户界面进行逻辑的处理
鉴于它非常灵活
那就是你的代码 那就是你想做的 你的应用有动画以及非常好的用户界面
但你的应用有些部分不需要可视指示 或可能不使用数据转换子系统 而且你想回收控制器的资源 当你不想要它了
我很想说 主线程 是唯一真正拥有 BusyController的子系统 我将会像这样去掉它 在初始化过程中从子系统中注册控制器 从好的方面想
这种方法不起作用 我将会用两个例子告诉你为什么
让我们回退一步 我们的BusyController 是参考UI 主队列和用户界面 然而 当我们用数据转换子系统注册时 很可能引用从数据结构转到这个对象 这意味着当我们去除这些引用 主线程有的参考
仍然会遗留一个 这意味着deinit 无法运行 这意味着它会被注册 被收集 接着因为被遗弃的内存你放弃了
然而 你们是技术熟练的开发人员 知道怎么来修复那个 弱引用 我会说 你们是对的 但是 这并不是故事的结局 因为这看起来并不像个真正的应用 图形对象复杂得多 使用有引用的对象是不常见的 还有一大堆其他对象 比如说octopus对象
我将继续从主线程中去除引用 不像之前 这不是废弃的内存 因为octopus对象知道它有引用
但若我们从上下文中 去除octopus对象时 数据转换系统 将会发生什么 它将去除 BusyController的引用 记住 这将在 运行的 因为那是deinit所做的
然后你有了一个问题 因为非常可能做到的 你需要同步一个调度队列 它拥有那个数据结构 你猜到了 我们最后会得到一个死锁
实际上 那个漏洞是非常普遍的 以致于我们为它写了一个断言 在这个新版本中
如果你在上一个发布版本中 运行那个代码 它会断言 在OS X或者模拟器上 你得到的奔溃报告有一个特定应用信息 指向你有的实际问题 所以你可以很简单地修好它 好了 现在我们知道 我们真的不想从deinit注销 我们如何修理它呢
我们通过我们的第三步来修理 作废 一个显示函数调用 在这个作废下 我们通过注册实现它
同样 因为我们有预设条件 让我们使用它们 因为这个对象 BusyController 确实应该在主进程中被管理 你想确保API的用户正确地使用它 所以你会想要一个预设条件 那只发生在主进程上 或者主队列上
但是那不是那样子的
我们还有最后一个问题 记住 这都是发生在主进程上的 你有这个子系统 数据转化子系统 它会告诉你状态变化 你可能有一些当你作废的时候发生 我们如何解决那个呢 我们想要追踪作废作为一个真的状态 那是什么意思呢 就是这个意思 你想要追踪作废 比如 作为你的对象中的一个布尔值 你记住当你做这个的时候
同时 让我们说一下更多的预设条件 确保加强 在你的对象被取消分配之前 它已经被正确地作废 它会帮助你找到漏洞
为什么这很有趣呢?因为现在
在你处理状态转化通知的代码中 我们可以观察对象被作废 实际把通知扔在地上 不是以令人尴尬的方式更新用户界面
好了 这大致就是这个复杂的例子 我希望你会回去看一下 你的应用和你的代码 尝试去发现这个模式能帮助你的地方 减少代码的复杂性和可能清除漏洞
这也应该不会让你感到惊讶 我们给你这个建议
GCD对象 它的真实目的是被同步地使用 遵循完全相同的模式
让我们以那种方式看待GCD对象
所以我们要记住第一步是设置 给调度对象设置是你可以做的所有事情 当你创建对象 所有你可以传递的属性 Matt前面已展示过标签和队列属性
在这里 我们也有一个调度资源 我们观察文件描述器 它获得一些属性 资源也有句柄 特别是代码中的事件句柄 当你在你的观察中发起的资源时 它会运行 那是事件等待 对于我们这里拥有的资源 那是当有可用的数据的时候
一旦你设置好了你的对象 它已经准备好了 你想要试用它和激活它
在这个新版本中 我们做了改进 对于API调用 调用激活 它曾经是用于调度资源 初始摘要就是那个意思 我们实际上暂停和激活 作为两个不同的概念 在重启激活中可以被多次调用 它只会执行一次
协议是当你已经调用了激活 你再也不会改变你的对象的属性
我们也发现以你使用资源的方式 创建队列 创建时让它们在初始化 处于非激活状态是很有用的 我们一家增加了一个新的属性 让你这么做 这被命名为initiallyInactive 一旦这个队列被创建了 你可以传递它 结束 以你喜欢的方式配置它 最后激活它
调度对象中的很多都不需要显式的作废 比如组或者队列 因为你停止使用它们的时候 它们就会变成非激活状态
这是非常不同于资源的 资源有显示的作废 它叫做Cancel
资源的取消就是做了你期望的事情 那就是你停止从你检测的东西中 获取事件 但这不是它所做的唯一的事情 它所做的第二件事是 如果你在你的资源中 设立了一个取消句柄 比如这里 它会在取消时运行在一个队列目标上
这实际上就是你想要摆脱的资源 你在监测它 比如关闭文件脚步 帧内存
最后但不是最不重要的一个 对资源的取消时当你的句柄被销毁 句柄就是关闭 它们获取对象甚至是资源自己 它们可以是读取过程的一部分
调用取消是你们破坏那读取循环的方式 这是为什么始终取消你的资源 是那么的重要
所以你记住前面所说的一点 我们增加了很多预设条件 在我们的代码中 因为我们想要确保 同步使用的对象永远是 以你可以期待的方式被使用
调度也是一样的 期待当你的对象被取消分配了 它是在一个不同的状态 调度期望你做两个事情 第一 你的对象是激活的 第二 它们不是被暂停的
为什么它是暂停或者是非激活的原因是 意味着你作为一个开发者 不要认为运行与其相关的代码是安全的 但是我们需要运行代码来摆脱这个对象
好了 今天我们看到了你可以如何 以数据流的方式思考你的应用 和你应该如何使用它 来将其划分为相当独立的子系统 使用值类型达到交流的目的
如果你需要同步状态 我们也向你展示了你可以怎么使用 调度队列来做这个 最后 当你有对象 它们是被使用过的 在一个同步性非常强的世界 如何使用激活和作废 来使得这个模式正确
这是一个链接 它向你们展示更多与这个演讲相关资源 一些相关演讲 若你对调度感兴趣 你们应该看看 因为它们非常的有趣
就是这样 这就是使用GCD进行同步编程 -
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。