大多数浏览器和
Developer App 均支持流媒体播放。
-
Grand Central Dispatch 的现代化用法
macOS 10.13 和 iOS 11 重新设计了 Grand Central Dispatch 和 Darwin 内核的协作方式,让您的 app 能够更加高效地运行并发工作负载。了解如何对您的代码进行现代化,以利用这些改进并优化对硬件资源的使用。
资源
相关视频
WWDC21
WWDC19
-
下载
(现代化GCD用法 如何粘附在内核上)
早上好 欢迎参加 现代化中心调度GCD用法演讲 我是Daniel Chimene 来自Core Darwin团队 我和我的同事们今天要跟大家谈谈 如何利用中心调度 在你的应用中获得最佳性能
作为应用开发人员 你会花成百上千个小时 为你的用户打造出色体验 利用你强大的设备
你想让你的用户拥有出色的体验 不仅是在一台设备上 而是在Apple的各种设备上
GCD的目的是帮助你动态地 调整你应用的代码 从单核的Apple Watch 一直到迷你核的Mac 你不必太过担心 你的用户运行的是哪种硬件 但会出现不确定的模式 可能会影响你代码的可扩展性和效率 无论是高端还是低端都一样 这也是我们今天要谈的内容 我们要帮助你确保 你放到应用中的代码 会给用户打造一种出色的体验 可以在各种设备间进行转换
你可能用过GCD API 比如异步调度 还有其它创建提示 以及给系统调度工作的API 这些只是并发性技术的一些接口 我们叫作中心调度GCD 今天我们要看看GCD底层的东西 这是一场充斥着各种信息的高级演讲 那么让我们立即开始吧 先看一下我们的硬件 我们的设备中出色的芯片 随着时间的推移速度变得越来越快 然而 速度快 并不只是因为芯片自身变快了 还因为它们在运行你的代码方面 变得越来越智能了 并且它们 随着时间的过去也在学习你代码的作用 以便更有效地运作
然而如果你的代码远离了内核 因为它完成了它的任务 那么它将不再利用 那个内核所创建的历史 你可能会把性能丢开 当你返回到内核上时 我们在我们自己的框架中 看到过这样的例子 当我们应用一些优化技巧时 我们今天也要讨论这些优化技巧 我们看到简单的变更就会很大程度上 加快速度 从而避免这些不确定的模式 (速度快了1.3倍 结合队列等级) 那么通过这些技巧 你可以用更少量的工作 给更多的用户带来高性能应用 今天我们要深入研究 我们系统底层是如何运作你的代码的 那么你可以调整你的代码以最好地利用 GCD所提供的功能 我们今天要讨论一些东西 首先我们要讨论如何最好地表达 并行和并发 如何选择最佳方式 向GCD表达并发 我们要介绍一下统一队列身份 这是GCD底层的一个重要改进 是我们今年发布的 我们最后要给你们演示 如何发现代码中的问题点 通过仪表 那么让我们先讨论并行和并发 那么…
为了演讲的目的 我们要谈谈平行 就是关于你的代码如何 同时在多个不同的内核上并行执行 并发是关于你如何构造 你应用的独立组件使其同时运行 那么区分这两个概念有一个简单的方式 就是明白 并行通常需要多内核 你想同时使用全部的内核 而并发甚至可以在单核系统中实现 它是关于你如何介入 作为应用的一部分的不同任务 那么让我们从并行开始谈 以及当你在写应用时如何使用它 那么 让我们想象一下你正在开发一款应用 而它会处理大量图片 你希望能利用Mac Pro上的多核 来更快地处理这些图片 于是你就把图片分别放到组块中 并且让每个内核
并行地处理这些组块 这就提升了你的速度 因为多核同时 处理图片的不同部分
那么你要如何实施呢?嗯 首先
你应该停下来思考 你是否可以利用我们系统框架 比如Accelerate框架有内嵌支持 可以支持高级图片算法的并行执行 Metal和Core图片 可以利用强大的GPU
嗯 假如说你已经决定自己实施 GCD会给你提供一个工具 可以让你轻松地表达这个模式 你向GCD表达并行的模式是使用一个 叫作concurrentPerform的API 它会让框架优化并行 因为它知道你正在尝试 利用所有内核实现并行计算 concurrentPerform是一个循环并行 会自动负载平衡系统中 所有内核的计算任务 Swift 当你与Swift一起用时 它会自动选择 运行你所有计算的正确情境 今年 我们把同样的功能 引入了Objective C的接口 通过调度应用自动关键字来调度应用 这替换了Q参数 允许系统 自动选择运行你代码的正确情境 那么现在让我们看一下另一个参数 即重复技术 这是在系统中 并行调用你的代码块的次数 在这里你要如何选择一个适当的值呢? 你可能认为适当的值应该是内核的数量 让我们想象一下 我们正在一个三核系统中 执行我们的工作 在这里你可以看到理想情况 也就是三个代码块在全部 三个内核上并行运行 但现实世界总是不会这么完美 如果第三个内核 被占用执行UI渲染会发生什么呢?
嗯 情况就会变成这样 负载平衡器必须得把那第三个代码块 挪到第一个内核上 以便执行第三个代码块 因为第三个内核被占用了 我们的CPU闲置 我们可以利用这段时间 来执行更多的并行工作 那么相反 我们的工作花的时间更长
那么如何修复呢? 嗯 我们可以增加重复计数
并给负载平衡器提供更多的灵活性
看起来不错 堵上那个窟窿了 但其实这里还有一个窟窿 就是在第三个内核上 我们也可以利用那段时间
那么正如提姆在周一的演讲上所说的 让我们把重复次数增加 增加到11
然后我们就填上这个窟窿了 我们的执行也更有效率了 我们用了系统上全部可用的资源 直到我们完成 这是一个非常简单的例子 要处理实际的复杂性 你得使用更大的量级 比如1000
你可以使用足够大的重复计数 以便负载平衡器可以灵活地 填补系统中的空白 并最大限度地利用 系统中可用的资源 然而你应该确保平衡 负载平衡器的消耗 相对于并行循环中每个代码块 做执行的有效工作
请记住每个CPU并不总是全部可用 系统中同时还运行着许多任务
此外 并不是每个辅助线程 都能取得同等的进度 那么回顾一下 如果你有并行问题 确保利用一切可用的系统框架 你可以用它们的能力来解决你的问题
此外 确保利用自动负载平衡 位于concurrentPerform内 给它提供一定的灵活性以最好地利用它
那么这是关于并行的讨论 现在让我们回到主话题上 即并发
那么并发
让我们想象一下 你正在写一个简单的新闻应用
你会如何构造它?嗯 你从把它拆解到 组成应用的独立子系统中开始 思考一下如何把新闻应用拆解 到其独立的子系统中 你可能有一个UI组件 是渲染UI的 这就是主线程 你可能有一个存储这些文章的数据库 且你可能还有一个联网的子系统 从网络上获取这些文章 为了给你一个更好的描述 关于这个应用是如何运作 以及如何拆解到子系统中 让我们在一个现代化系统上 形象化一下那是如何同时执行的 那么假如这个时间轴 在CPU轨道的顶部显示 让我们想象一下 我们只剩下了一个CPU 其它CPU都忙于其它任务 我们只有一个可用的内核 无论什么时候只能有一个线程 运行在那个CPU上 那么当用户在新闻应用中点击按钮 并刷新文章列表时会发生什么? 嗯 这些界面会渲染对那个按钮的响应 然后给数据库发送异步指令 然后数据库就决定它需要刷新文章 它会对联网子系统选择另一个命令 然而在这点上 用户再次触摸应用 因为数据库脱离应用的主线程 完成了执行 OS可以立即切换CPU处理UI线程 它可以立即响应用户 而无需等待数据库线程执行完成
这就是把工作从主线程上脱离的好处
当用户界面完成响应时 CPU就可以切换回数据库线程 然后完成联网任务 像这样利用并发 可以让你创建响应性的应用 主线程总是响应用户的行动 而无需等待应用的其它部分完成 那么让我们看看对于CPU来说 是什么样的 上边的这些白线 显示的是子系统之间的内容切换 接触开关是当CPU 在不同的子系统之间 或组成你应用的线程之间进行切换时 如果你想这在你的应用中是什么样的 你可以使用仪表系统进行追踪 可以给你显示CPU和线程正在做什么 当它们在你的应用中运行时 如果你想了解更多信息 你可以参看去年的 “深度解析系统追踪”演讲 仪表团队描述了你该如何使用系统追踪 那么这个情境切换的概念是 并发的能力来自哪里 让我们看看何时会发生这些情境切换 以及导致情境切换的原因 嗯 当高优先级线程需要CPU时会发生 正如我们之前看到过的 UI线程会先于数据库线程 当线程结束当前任务时 或等待取得资源时 或等待异步请求完成时也会发生 然而 通过并发的强大力量 也会伴随着强大的责任 你拥有太多好东西了
假如说你正在网络 和数据库线程之间切换 在你的CPU上 切换几个情境没问题 这就是并发的能力所在 你可以在不同的任务之前切换 然而如果你要做上千次切换 连续迅速切换 你就有大麻烦啦 你就开始丢失性能 因为这里的每一条白线 都是一个情境切换 情境切换的消耗也增加了 并不只是说执行情境切换的时间 还有内核所创建的历史 它必须得在每次情境切换后 恢复那个历史
你可能还会有其它影响因素 比如可能还有其它线程 正在排队等待获取CPU
每次切换情境时 剩余的线程队列都必须等待疏通 那么你可能会被队列中的其它线程 超越而导致延迟 那么让我们看看 导致情境切换次数过多的原因 那么今天我们要讲的是三个主要原因 第一是反复等待独占资源 在独立的运作之间反复切换 并且在线程间反复跳过某个运作 你注意到我反复几次提到了反复这个词 我是刻意这么说的
情境切换几次是没问题的 这就是并发的作用 也是我们赋予你们的能力 然而当你重复切换太多次时 消耗就开始增加了
那么让我们来看第一种情况 也就是独占资源 什么时候会发生这种情况? 嗯 这种发生的主要情况是 当你有一个锁定 而大量线程都尝试获取那个锁定时
那么如何了解你的应用中 是否有这种情况发生? 嗯 我们可以返回到系统追踪 我们可以在仪表中 形象化具体是怎么样的 那么假如说它显示我们有许多线程 只运行很短的时间 并且它们会在一个小串联中相互转换 让我们主要关注第一个线程 看看它会告诉我们哪些东西
我们有这个蓝色线 它表示的是线程在CPU上的时间 而红色线表示执行系统调用的时间 在本例中 它会执行一个无声文本系统调用
这显示出它绝大部分时间 都在等待无声文本变为可用 在内核上的时间很短 只有10毫秒 系统中有大量情境切换 通过顶部的情境切换条显示
那么是什么原因导致了这样的情况? 让我们返回去看那个简单的时间轴 了解如何…如何获取情境… 如何用完过度的负债 那么你可以及时了解到这种阶梯式模式 每个线程都只运行很短的时间 然后就把CPU让给下一个线程 以此类推 然后等待很长时间 你想让你的应用看起来像这样 你必须 CPU每次只能专注于一件事 完成它 然后再执行下一个任务 那么这里发生了什么 才导致了那种阶梯? 让我们放大其中一个阶梯
那么在这里我们正在专注两个线程 绿色线程和蓝色线程 并且顶部有CPU 我们在这里添加了一个新的锁定追踪 表明了锁定的状态以及哪个线程拥有它 在本例中 蓝色线程拥有这个锁定 而绿色线程正在等待 那么当蓝色线程解锁时 那个锁定的所有权就转给了绿色线程 因为它是队列中的下一个 然而当蓝色线程转回来再次获取锁定时 它不会成功 因为绿色线程预定了锁定 它强制进行情境切换 因为我们现在必须做其它事 我们切换到了绿色线程 那么CPU就可以 结束锁定 我们可以反复这个过程
有时候很有帮助 你希望等待锁定的每一个线程 都有机会获取资源 然而如果锁定的工作方式 不一样会发生什么呢? 让我们再从开始做 通过查看一个不公平的锁定会做什么
那么这一次当蓝色线程解锁时 锁定并没有被预定 锁定的所有权被公开拍卖了
蓝色线程可以再次获取锁定 并且它可以立即重新获取 并保留在CPU上 而不需要强制进行情景切换 那可能会导致绿色线程很难 有机会获取锁定 但它减少了强制情景切换的次数… 是蓝色线程为了重新获取锁定 而发起的强制切换
那么回顾一下当我们谈到锁定竞争时 你实际上是想确保 衡量你的应用和系统追踪 了解是否存在问题 如果你这么做了 这种不公平的锁定 通常适用于对象、属性 或全局状态 你的应用 可能会发生的锁定会跌很多很多次
当提到锁定时 我还想谈另外一件事 就是锁定的所有权 那么请记住我们之前的锁定追踪 运行时间知道接下来是哪个线程 会解锁锁定 我们可以利用那种能力 自动计算你应用中的优先级反转 在等待锁定的线程 和拥有锁定的线程之间 甚至启用其它优化 比如让CPU转到拥有锁定的线程上 稍后Pierre会跟大家讨论这个话题 当我们谈同步调度时
我们通常会有这个问题 哪些基元有这种能力而哪些没有呢? 让我们看看哪些低等级基元可以实现
那么拥有单一 已知所有者的基元有这种能力 比如连续队列和OS的不公平锁定
然而不对称基元 比如调度信号和调度群就没有这种能力 因为运行时间不知道哪个线程 会单一化子基元
最终 拥有多个所有者的基元 比如私人化的并发队列和读或写锁定 系统就不用利用那些 因为它们不是只有一个所有者 当你选择基元时 要考虑你是否 涉及多基元的线程交互 比如拥有较低优先级 后台线程的高级UI线程
如果是这样的话 你可能想利用基元的所有权 确保你的UI线程 不会因为等待 较低优先级的后台线程而延迟
那么总的来说 这些效率低的行为 通常会出现在你应用的属性中 仅仅通过看代码是很难发现这些问题的 你应该在仪表系统追踪中进行观察 形象化应用的真实行为 以便你可以利用正确的锁定执行任务 那么我刚才谈了 情境切换列表中的第一个原因 也就是独占资源
要谈 应用可能会拥有过度情境 切换的其它方式 让我们欢迎我的同事 Daniel Steffen上台 跟大家谈谈如何通过GDC管理并发 以避免这些陷阱
好的 谢谢Daniel (使用GCD处理并发) 那么我们今天要讲很多内容 所以我不会 讲太多GCD的基本原理 如果你刚接触这个技术 或者需要回顾一些知识 这里有一些WWDC之前发布的演讲 是前几年我们做的演讲 详细介绍了GCD及其改进 那么我鼓励你查看这些视频
我们还会涉及GCD的一些基本概念 让我们从调度连续队列开始讲
这真的是GCD中基本的同步基元 它提供互斥以及FIFO排序 这是刚才Daniel提到的其中一个 有序的和公平的基元
它在队列运作中有一个并发原子 所以多线程 排队同时处理队列是没问题的 以及系统提供的单一DQS线程 在队列之外执行异步运作也是没问题的 那么让我们实际看个例子 在这里我们创建了一个串行序列 通过调用调度队列构造器 它会给你留下一个印象 只要你还没有用它 它就会存在于你的应用中 现在想象一下有两个线程 同时出现并调用queue.async方法 来向这个队列提交一些异步任务 我们刚提到过 它寻找能实现此功能的多线程 而项会进入队列 以便它们能出现
因为这是个异步方法 这个方法会返回并且线程可以继续 那么也许这第一个线程 最终会调用queue.sync 这就是你与队列同步交互的方式 因为这是个有序基元 它的作用是它将作为 队列中的一个队列占位符 以便线程可以等待直到轮到它
现在有了这个自动辅助线程 它可以执行异步工作 直到你排到那个占位符那儿 那时候队列的所有者将会转交给线程 在queue.sync中等待 以便可以执行自己的代码块
那么下一个概念是调度源 这是GCD中的事件监控基元 在这里我们设置了一个 用于监控一个默认基元的可读性 如果你有读取资源构造器的话 你在队列中传递它 就是源的目标队列 也是我们执行源的事件处理的地方 在这里只需要从默认描述符中读取 这个目标队列还是你可能会放置 应该与这个运作 一起序列化的任务的地方 比如处理所读取的数据
那么我们给源设置了取消处理器 是源如何将它们 实施为无效模式的方式 最终当一切都设置好之后 你需要调用源并激活以开始监控
那么值得注意的是源只是 OS中一个更通用的模式的实例 其中包括可以按照你指定的目标队列 向你提交事件的对象 那么如果你熟悉XPC 那可以作为 那个XPC连接的另一个例子
并且值得注意的是我们今天 告诉你们的与源有关的全部内容 一般来说都可以应用到全部这种对象上
那么把这两个概念结合到一起 我们就得到了一个目标队列等级
那么在这里我们有两个源 及其相关联的目标队列 S1、S2 队列是Q1和Q2 我们构造一个小的树形结构 解决这种情况 通过添加另一个连续队列 通过在底部添加互斥队列EQ 我们的实现方式是通过把可选目标参数 传给调度队列构造器
那么这就为整个树形结构 提供了一个共享单一互斥情境 每次只能执行其中一个源 或队列中的一项 但它为队列1和队列2 保持了独立的单项队列顺序 那么让我们具体来看看我所表达的意思
在这里我有两个队列 队列1和队列2 它们以指定顺序排列 因为我们底部有这个额外的连续队列 还有执行 那么它们将在EQ中执行 并且会有一个单一辅助线程执行这些 为你提供互斥属性的项 每次只能执行一项 但正如你所看到的 两个队列中的项可以穿插执行 并保留各自在原始队列中的顺序
那么我们今天涉及到的最后一个 概念就是服务质量
这其实是以前详细谈过的一个概念 尤其是在2014年的能力性能 和诊断演讲上 那么如果这对于你来说不熟悉 我建议你参看那场演讲的资料 但我们今天要讲的主要是 优先级的抽象概念
我们会用到术语QOS和优先级 在接下来的演讲中会交替出现
我们的系统上的服务级别有四种质量 最高的是用户交互性UI、 到用户发起(或IN)、效用(UT) 到后台(BG) 最低优先级
那么让我们看看要如何结合服务质量 通过我们刚看过的目标队列等级 在这个等级中 树结构中的每个节点 其实都可以有一个与之 相关联的服务标签质量 那么比如说源2可能与用户界面相关 它可能会针对事件被监控 一旦触发事件 我们就应该更新它们的UI 那么有可能我们想把这个UI标签 放到源上 另一个常见的用例是 把标签放在互斥队列中 以提供一系列的执行 从而树形结构中的全部都能在这个 等级下执行 那么本例中就是UT
现在如果这个队列中的任意其它项 被触发 比如源1 我们将使用这个属性结构的等级 如果它没有 与自己相关联的服务质量的话
源触发其实就像 从内核中执行的异步运作 在发生之前也一样 我们会排在源处理器队列的最后 最终进入互斥队列执行
对于用户空间的异步运作 你的服务质量 通常是由那个 叫作queue.async的线程决定的 现在我们有一个用户发起的线程 把项提交 到队列中 并最终执行到E队列中 现在也许我们有源2 与这个高优先级的UI相关事件一起 源2执行其事件处理器 并把它的事件处理器排到了EQ中
那么现在你会注意到我们 处于优先级倒置的情况中 我们的队列中有三个项 最后一个项的优先级很高 前边是一些低优先级项 并且必须要按顺序执行 系统为你解决了这种倒置 通过一个拉动一个 比当前队列中所有项都高的 最高优先级辅助线程
我们一定要记住右边的这个树形图 因为稍后它会再次出现
让我们继续我们的主话题 即如何使用我们刚学到的知识 来向GCD表达优良的并发间隔
让我们返回去看Daniel 刚刚介绍过的新闻应用 稍微多关注一点联网子系统
在联网子系统中 你必须监控 内核中的一些网络连接 有了GCD 你可以用调度源 和调度队列实现 就像你刚看到的那样 但当然了在任何网络子系统中 你通常不仅仅有一个网络连接 你会有许多网络连接 它们会复制同样的设置 那么让我们主要关注右边的这三个连接 看看它们是如何执行的
如果触发了第一个连接 跟我们刚看到的一样 我们会排在那个源的事件处理器的最后 并将其排入目标队列 当然如果同时触发了其它两个连接 它们仍会复制 然后你将结束这三个队列 而事件处理器排在最后 因为你在底部有这三个独立的连续队列 你其实已经请求系统给你提供 三个独立的并发情境 如果全部三个同时被激活 系统将强制创建 三个线程用于执行时间处理器
现在这也许就是你想要的 也许正是你所要追寻的效果 但常见的情况是这些事件处理器很小 而且只能 从网络中读取一些数据 并把它排入一个常见的数据结构中 此外正如我们之前所看到的 你并不只是有三个连接 你可能有很多很多连接 如果你的子系统中有许多网络连接的话 那么这可能会导致这种 情境切换模式的出现 就是Daniel刚谈过的过度的情境切换 就是你执行少量工作 情境切换到另一个线程 再执行少量工作 如此反复 那么我们该如何改善 这个例子中的这种情况呢? 我们可以应用我们刚谈到的那个 单一互斥情境的概念 只需要在底部放一个额外的连续队列 并形成一个等级即可 你可以获得 全部这些连接的单一互斥情境 如果它们同时被触发 将会发生跟以前一模一样的情况 事件处理器将会排在目标队列的最后 但因为底部有了这个额外的连续队列 它是单一线程 它会按顺序执行这些事件处理器 而不是由我们之前的那个多线程来执行 那么看起来像是个非常简单的修改 但这种修改却 将我们自己某些代码的性能 提升了1.3倍 正如Daniel之前提到过的一样
那么这是如何避免问题模式的一个例子 关于在独立运作之间反复切换的问题 但它却归入了一个通用标题 叫作在应用中避免 多余的和不受控制的并发
解决办法之一就是同时激活许多队列 其中一个例子是 我们刚才看到的那个需要独立的源模式 如果你有独立或对象队列也可以解决 如果你的应用中许多对象 都有自己的连续队列 并且你同时给赋予它们异步运作 你就可以得到一模一样的效果
如果你有许多工作项 同时提交到全局并发队列 也可以解决 特别是如果有工作项阻塞时 全局并发队列的运作方式是 它会纠正更多的线程 当现有线程阻塞了 给你提供并发的持续性良好等级 但如果这些线程再次阻塞 你可以用一个叫作线程爆炸的东西
在这个话题中 我们要接触到一些细节 在2015年的“用GCD创建响应性 和高效率应用”演讲中有详细介绍 那么如果你不熟悉这些 我建议你参看那场演讲的视频
那么如何选择适当数量的并发 以避免这些问题模式呢? 以前我们推荐给你的一个方法是 给子系统使用一个队列
那么返回到我们的新闻应用 我们用户界面已经有了一个队列 就是主队列 我们可以给网络连接选择一个连续队列 并另外给数据库子系统选择一个 连续队列
但我们今天学了另一种 思考这种情况的新方式 其实就是给每个子系统 使用一个队列等级
这就给子系统提供了一个互斥情境 你可以让队列中余下的事件 单独进行子构建和拥有子系统 只把那个网络队列 或数据库队列作为目标 就是位于队列等级底部的那个队列
但那可能是一个太过简单的模式 对于复杂应用或复杂子系统来说 这里的重点是 在应用中拥有固定数量的连续队列等级 拥有额外队列等级对于 复杂的子系统来说很有必要 比如第二队列等级 用于处理较慢的工作 或较大的工作项 那么第一个队列等级 就是主要的队列等级 可以保持子系统响应 外面进来的请求
这个情境中需要思考的另一个重点 是提交给那些子系统的工作的间隔
你想使用相当大的工作项 当你在子系统中移动 以实现我们之前看到过的那种情况 也就是CP有足够长的时间 执行你的子系统 以达到一种高效的性能状态
一旦你处于子系统内部 比如这里的网络子系统 就有必要把子系统细分为 更小的组块项了 并且用更精细的间隔来改善 那个子系统的响应能力 比如你可以拆分你的工作并重新异步 到你队列等级中的另一个队列 那并不会导致情境切换 因为你已经在那个子系统中了
那么总的来说我们在这部分 学到了什么? 我们了解了如何把队列和源 组织到连续队列等级中 如何使用固定数量的连续队列等级 使GCD获得一个不错的并发间隔 以及如何合适地拆分你的工作项 用于之前讲的并行工作 和这里的并发工作 在子系统内以及子系统之间 好的 我要把舞台交给Pierre 他会深入探讨我们对GCD的改进 使其总是 在单一线程上执行队列等级 以及如何现代化你的代码 来利用这个改进
谢谢Daniel
那么的确我们今年 彻底改造了GCD的内部构件 以剔除某些多余的情境切换 并执行单一队列等级 就像Daniel在单一线程上 所展示的那个例子一样 为此我们创建了一个新概念 我们叫作统一队列身份 让我们来实践一下 我们会逐步带你认识它是如何工作的
那么其实这部分内容… 开始…将主要关注单一队列等级 比如Daniel刚展示的那几个 然而我们会处理一些简化的 单一队列等级 它们的顶部有源 底部有互斥情境 GCD内部的注释并不是为了 那部分演讲而准备的
那么当你创建情境时 你通常会调度队列构造器 那只会给你的应用留下一个印象 表明那是一个注释 你可能要做的第一件事 就是给它调度最近编码的项
那么你的应用中将有代码会在这里 并在队列中排队
以前当那种情况发生时 我们通常会请求 给系统调度一个匿名线程 并且那个匿名线程必须要做的决心 稍后会在你应用内部发生
在这种情况下 我们进行了修改 我们要做的是创建我们的计数器对象 绑定到你的队列的统一队列身份 恰好要在内核中代表你的队列 我们可以给那个对象绑定 执行工作所需要的等级 那么在这里就是备份
那会导致系统请求线程 线程请求 就是幻灯片上的虚线 有时可能不会实现 因为这里有一个后台线程 也许系统已加载了足够多 甚至不值得再提供给你一个线程
稍后你应用的其它路径 可能实际上会尝试让更多工作排队 这里有一个UT项 优先级稍高
我们可以使用队列身份 内核中的统一身份 来查看和解决优先级倒置问题 并利用那个线程请求的优先级 也许那就是系统所需要的小的推进 要在此给你一个线程来执行你的工作 但这个线程是排程器中的队列 目前并不是听候调用 也不会执行 原因是你的应用中有另一个线程 与队列交互并同步工作 以较高的优先级 甚至通常会被遮住
现在我们有了那个统一队列身份 我们实际上可以 因为那个线程必须阻塞占位符的排队 这是Daniel之前刚跟你们谈过的 我们可以阻塞 那个线程在统一队列身份上的同步执行 对于异步工作我们采取了同样的方式 但现在我们以单一身份 统一了队列的异步 和同步部分 我们可以应用一个优化 并直接切换 阻塞你通过排程器队列的线程 并注册队列延迟 就是Daniel在最开始 讲到排程器时跟大家介绍的 那么这就是统一队列身份 处理异步和同步工作项的方式
现在我们如何把它用在事件上? 为什么有用呢? 那么这是我们一直在用的小树形图 让我们看看这些源的创建
当你用makeResource工厂按钮 创建源时 你就设置了一堆事件 分别有适当的处理器和属性 但有意思的是当你激活对象时 会发生什么
就在那时我们才意识到 那个效用就是QOS 你的源将在那个QOS上 一直执行处理器 因为它是从你的队列等级中继承来的 我们现在还知道通过新系统 处理器最终会 在队列执行互斥情境中执行 现在将会在前端注册源 通过我稍早之前谈到过的同步统一身份
如果我们看一下树形图上 较高等级的UI QOS源 我们的创建方式与第一个十分相似 除了当你设置处理器时 要指定你想要的QOS 有意思的是在激活状态时 所发生的情况 这是我们在快照中时 和以前一样 当我们从你的等级 获取统一QOS之后 在这里我们从你的暗示中获取它 我们回想一下 它们将在 同一个执行情境中执行两个源 并将第二个源再次注册到最前面 第二个源在内核中有统一身份
那么我们尝试要做的是 通过非常复杂的身份解决 的是一个我们在OS之前的阶段 存在的一个问题 相关运作会脱离老线程 让我们看看它通常是如何运作的 那么请记住这是我们的队列等级
让我们打开你在本次演讲中 多次看到过的时间轴 顶部是CPU 但现在有一个新追踪 执行队列卡将表示 在任意时刻该队列上正在执行什么
那么这是运行时间真正的运作方式 在我们进入macOS Sierra 和iOS 10之前的阶段 那么让我们看看如果第一个源 失败了会发生什么 正如我所说的 之前线程请求是异步的 我们会请求一个异步线程 在线程上提交事件 然后我们会查看事件
当我们在应用内查看事件时 我们才会意识到这个事件 必须要在队列上运行 然后我们会在让事件处理器排队
但因为队列是未经声明的队列 线程实际上可以变成那个队列 并开始为那个源执行指定的处理器 我们会这么做 现在有意思的是 当触发优先级较高的第二个源时 会怎么样? 实际上情况一模一样 因为它在这里是一个等级QOS 优先级较高 这就是你现在正在执行的
我们会打开一个新的异步线程 在线程上提交那个优先级较高的事件
并查看那个事件是什么意思 直到那时我们才注意到 那是同一个一模一样的队列等级 然后把处理器排在我们 预先占用的处理器之后 正如你所看到的 我们关闭了第一个情境切换 就是那个优先级较高的事件的情境 但我们不能促进进展 因为跟第一次不同 第二个线程不能接受 已经与某个线程相关联的队列 我们不能接受它 那么线程就结束了 正如Daniel所阐述过的 这就是你再次切换情境的原因之一 这是我们所做的工作 我们把情境切换回第一个线程 就是实际上取得进展的那个 我们执行第一个处理器余下的部分 并最终执行第二个处理器
那么正如你所看到的 我们使用两个线程和两次情境切换 你实际上不想针对一个 单一执行情境这样做
我们在macOS High Sierra和iOS 11中 用统一身份修复了那个问题
我们摆脱了那个线程
当然了 我们还摆脱了 以前的那两次情境切换 那是多余的
这当然很重要 因为 跟Daniel通过那个触摸事件给你展示 占先及优先级时所发生的不一样 我们可以利用 我们实际有两个线程的事实 相对独立并且响应性更强 在这里我们没有从任何情境切换中受益 因为这些事件处理器 不管怎样S1和M2必须得按顺序执行 那么提前知道那个事件 并不会有什么帮助
如果你看一下…今天的流程 看起来更应该是这样的
这里发生了什么?
那个流程中最重要的事 就是现在如果你查看线程 它叫作EQ 因为它是统一身份的一个部分 线程和EQ从根本上说是同一个对象 而内核知道它其实正在执行队列 在CPU标签上反映出来了 你再也看不到事件了 那就会运行你的队列
然而你可能会问 我们该如何设法提交事件呢 提交第二个事件而不需要帮手 这是个好问题
当事件被触发时 现在我们知道它会在哪儿执行 你会在哪儿处理它 我们只需要标记线程 不需要帮手 在第一个可能的时间 我们就会注意到那个线程被你标记了 有待定的事件 当我们让事件出列时需要隐藏时间 藏在第一个处理器结束之后 我们可以从内核中获取事件并查看 然后把它们的处理器 排在你的等级队列中
那么我们为什么 要给出这么复杂的解释呢? 原因是你 这样可以理解如何最好地利用 运行时间行为 因为很清楚 运行时间使用了 你提供给我们的每一个可能的暗示 在你的应用中优化行为
并且进入按钮知道如何暗示 以及何时正确地暗示运行时间 因此我们就做除了正确的决策
这就导致我今天跟大家讲 你们应该如何处理现有代码基 以最大限度地利用 我们重新创建的内核技术
现在实际上只需要两个步骤 就可以充分利用那个技术了 第一个步骤是在激活后没有变化 第二个步骤是特别关注 目标队列等级
那么这是什么意思?
没有变化越过激活其实是说 当你在调度对象上有任意属性时 你可以发送它们 一旦你激活对象 你就应该停止改变它们 第二个例子
这是我们在本场演讲中 看到过多次的源 那个能力
并且你会设置一堆属性、处理器; 事件处理器、同意处理器 你可能还有注册处理器 你甚至可以修改几次 这没问题 你可以改变主意
当你激活源时
这里的联系是你应该停止改变你的对象 诱惑力很大 事后 比如说修改源的目标队列 那将会导致问题 原因就是我早些时候所展示过的 在激活时 我们会给你对象的属性生成快照 将来我们将按照那个快照做出决策
如果你在事后修改了目标队列等级 它会阻碍那个快照 使其失效 那将会导致一些列非常重要的优化失败 比如优先级倒置避免GCD 我之前展示的 同步调度的直接切换 这些都是我们刚刚看过的防御型 和可交付的优化
我会坚持 Daniel之前的观点 就是你们中有许多人可能从来不需要 在你们的应用中创建一个调度源 这没问题 这就是它的运作方式 你实际上很可能在系统框架中 大量使用它们 每次你有框架时 你就会调度队列 因为 它们会以你的名义在队列上 异步处理一些通知 在底层 它们拥有其中一个源 那么如果你修改了系统的假设 你实际上也会打断 全部这些 优化
那么我希望很清楚地表达了一点 把你的等级作为目标 非常重要 你必须保护它
这是什么意思?需要怎么做?
第一种方式是一个很简单的建议 就是当你创建等级时 从底部开始往顶部创建
当你看到幻灯片上的这个树形图 创建好之后 正如你所看到的 那儿有一些很宽的窟窿 那就是你的目标队列关系 如果你按那个顺序创建 哪个也不会被改变
然而 当你有一个很大的应用或当你隐藏框架 并把其中一个队列传给 你们公司的另一个团队时 你可能更希望得到较多的保证 你可能想锁定这些关系 以便没有任何人可以在事后改变它们
实际上你可以 通过我们的一种叫作 设置队列等级的技术实现 我们去年引入了这种技术 实际上如果你正在使用Swift 3 你就可以停下来听我讲 因为你已经在那种形式中了 那是你正在生存的唯一的世界
然而如果你有一个现有的基于云的应用 或你用的是Swift的较老版本 你需要一些额外的步骤
那么让我们重点看一下 Q1和EQ之间的关系 当你用Objective-C创建时 你的代码很可能会是这样的 你创建你的队列 然后第二步 把Q1的目标队列设置为EQ 这并不会保护你的队列等级 任何人都可以再次调用调度目标队列 并打乱你的全部假设 这可不妙 有一个简单的步骤可以 修复那段代码让它变得安全 就是采用我们去年引入的一个新API 即用调度目标队列的创建 这是一个单一的自动化步骤 将创建队列 设置队列等级高度并保护它
就是这样 这就是这两个步骤 通过这两个步骤你可以 很好地 非常简单
另外 与Daniel刚才给你们演示的 那个突变的例子有点类似 寻找何时做出了错误的决定 非常具有挑战性 尤其是在大的基于云的应用上 我们发现在现有的基于云的应用中 检验全部代码是非常困难的 这也就是我们为什么 创建一个新GCD性能仪表工具 来寻找现有应用中的问题点的原因 我将把舞台交换给Daniel 他会给你们做演示
谢谢Pierre
好的 让我们开始吧 请注意我们即将看到的这个 GCD性能仪表 目前Xcode 9中还没有 但你们很快就能用了 我们即将发布Xcode 9的种子
那么对于这个演示 让我们具体分析一下 我们的示例新闻应用的执行 如果你点击底部的这个 连接按钮会发生什么呢 这个应用会向服务器 创建若干网络连接吗 以便读取URL列表 然后就显示在WebViews中 无论何时点击刷新按钮时 那么让我们深入Xcode查看 我们是如何设置这些网络连接的
那么这是Xcode中的创建连接方法 非常简单 我们有一个循环 也许只是创建一些套接口 并把它们连接到我们的服务器 我们通过其中一个调度读取 源监控那个套接口的可读性 就是我们在本场演讲中 多次见到的那个源 在这里查看API是受信任的 然后我们可以为那个调度源 设置事件处理器代码块 当套接口可读时 我们就从中读取 通过读取系统调用 直到没有可用数据 一旦我们有了数据 我们就把它传给我们的数据库子系统 通过这个process 0方法
那么让我们创建并运行 并执行这个应用的系统追踪 了解它是如何执行的
那么我们在仪表中 在系统追踪中 除了系统追踪中的常见追踪 我们还添加了这个新的GCD性能仪表 当我们点击它时 我们可以看到许多性能事件 被报告为有性能问题 其中一个就是这个激活事件后改变 我们还可以看到随着时间轴 何时数量众多 你还可以点击这里的一个事件 比如在激活事件之后点击这个改变目标 这个列表将会把我们直接带到那儿 如果你想获取更多详细信息 我们可以公开仪表右侧的向后追踪 那将会给我们显示这个事件 具体发生在你应用中的哪个位置 那么在这里比如说是 在我们的创建连接方法中
如果我们双击这个框架 仪表将会直接给我们显示 问题所在的代码行 这是几上是一个目标队列调用 是在激活事件之后发生的 就像刚跟你说过的这个模式 要修复这个问题 我们可以直接进入Xcode 用在Xcode中打开文件快捷按钮 打开文件 那么在这里 我们是在调度目标队列代码行 的确是在这行 在事件处理器有调度源 设置发生在激活之后 那么在这个例子中 非常容易修复 我们只需要把这两行往下挪 然后就修复了问题 我们在设置源之后激活而不是之前 那么让我们返回仪表并查看 我们现在能在系统追踪中看到什么 看起来跟以前一样 除了当你点击GCD性能追踪时 你将看到没有检测到 任何严重的性能问题了 如果你用了这个仪表 你应该会看到这个结果 那么当然这对于这个应用来说很简单 你可能需要做一些工作
那么让我们重点看一下 应用中的点追踪 这会给我们显示许多网络事件处理器 这些是我们应用中的源事件处理器 那么你要如何设法 在仪表中显示这些呢? 实际上理解起来很有意思 因为这是你可以应用到 你自己的代码中的东西 以在仪表中了解它是如何执行的
嗯 让我们返回到Xcode 中的创建连接方法 当我们设置源及其源事件处理器时 我们就对那个 事件处理器的执行感兴趣了 并尝试了解它的时间安排 为了在仪表中看到它 我们增加了 kdebug signpost start函数 放在处理器的开始 以及kdebug signpost end函数 放在处理器的末端 这就是它所需要做的工作 让这段代码 突出显示在仪表系统追踪的点追踪中 那么如果你切换回仪表 就是点追踪顶部的这些红点 我们可以看到在向后追踪中 它把我们的事件处理器 与其中一个事件相匹配了 如果你放大点追踪中的一个 有意思的查看区域 你可以看到有许多事件处理器 非常紧凑地发生 鼠标悬浮在其中一个处理器上 我们实际上可以看到 它们只执行了非常短的一段时间 弹出会告诉我们它所执行的时间长度 我们甚至可以看到 有时会有重叠的事件处理器 同时并发执行 那么这是我们应用中的 其中一个潜在的不利并行 是看起来 不会在代码中导致并行的东西 但实际上却以并发方式 运行或多线程运行 并导致了潜在的额外情境切换
那么为了更好地理解 让我们在仪表中拉动线程 以及执行这段代码的系统追踪
在这里我已经突出了三个辅助线程 就是执行这些事件处理器的辅助线程 我们之前可以看到 它们在这段时间待命执行 这也是它们运行的时间 但在这里我们可以看到它们再一次 在这个区域运行了很短的时间 并且我们可以识别它们 正在发起这些读取系统调用 是我们之前在事件处理器中看到的 我们可以通过再一次查看 向后追踪获取更多信息 并查看 是的就是我们在 调用那个读取系统调用 且它从我们的套接口中 读取了97个字节
查看其它线程 重复了同一种模式 你可以看到那里发生的是 同一个读取系统调用 或多或少都在同一个时间框架中 那么也就是在第二个线程上 或第一个线程上 它们实际上做的是同样的事 重叠了
如果这些事在单一线程上执行 对于我们的程序来说会更棒 在这里我们并没有真正从并发中受益 因为我们执行的是这种很短的代码 很可能坏处大于益处 从添加这些额外情境切换的角度来说 那么让我们应用我们之前看到的模式 来修复这个示例应用中的这个问题 返回到Xcode 让我们看看我们要如何 为我们的那个源设置目标队列
那么当你在这个函数框架顶部 创建这个队列时 正如你所看到的 我们很简单地就实现了 通过正确地调用这个调度队列 那会创建一个独立的连续队列 并不会连接到我们应用中的任何东西 这根我们之前看到的例子一模一样 就是那个联网子系统的例子 那么让我们修复这个问题 通过在所有队列的底部为全部 连接添加一个互斥情境 我们通过添加 或通过切换到 调度队列 调度队列是通过 之前给你介绍的这个目标函数创建的
在这里我们添加了调度队列 创建了这个目标 并且我们将单一互斥队列 作为全部队列的目标队列 并且这个连续队列是我们 在其它地方创建的 然后我们再次创建并运行 并再次查看系统追踪 现在看起来很不一样了
在这里我们仍然有同样的点追踪 我们仍然能看到同样的网络事件发生 但正如你所看到的 那个追踪中不再有重叠事件了 并且有一个单一辅助线程 执行这段代码 如果我们放大其中一个集群 我们可以看到这实际上是 那个事件处理器的许多实例 接连不断地执行 这正是我们所期待的结果 当你再放大其中某个事件时 你可以看到它仍然执行 相当短的一段时间 并做出了同样的读取系统调用 但现在问题小多了 因为这全部发生在一个单线程上
(找到问题点) 那么这看起来可能像是一个很简单、 很微小的修改 但值得注意的是 正是这种小的调整导致 我们自己的某些框架代码的性能 提升了1.3倍 这正是Daniel在演讲 一开始就指出来的 那么像这样的很小的修改 可以导致很大的不同
好了 让我们回顾一下 我们今天演讲的内容 Daniel在一开始就跟大家具体谈了 在不必要的情况下不离开内核的方式 永远比现代化的CPU要重要 从而它可以达到最高效的性能状态 我们看了调整能力 工作负载的劳动力的重要性 以及在应用中的子系统之间 以及这些子系统内部的工作转移 我们还谈了如何通过GCD 选择不错的并发间隔 通过在应用中使用固定数量的连续队列 并且Pierre跟你们一起演示了 如何现代化你的GCD使用 以充分地利用OS中对硬件的改进 最后我们了解了如何使用仪表 在我们的应用中发现问题点 以及如何修复它们
要获取更多关于本场演讲的信息 请参看这个URL 这里有GCD的文档链接以及演讲视频 本周还有一些相关演讲 可能值得一去 已经结束了的有Core ML介绍 另外两场 会帮助你处理并行 和计算任务 在你的应用中 正如我们在一开始所谈到的那样 最后两场可以帮助你处理 更多的性能分析 和改善应用的不同方面 就这样了 我想谢谢大家的参与
如果你有任何疑问 欢迎来我们的实验室咨询 -
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。