大多数浏览器和
Developer App 均支持流媒体播放。
-
Swift 并发功能:幕后故事
敬请深入了解 Swift 并发功能的细节,探索 Swift 如何在提高性能的同时提供更大的安全性,避免数据竞争和线程爆炸。我们将探讨 Swift 任务与 Grand Central Dispatch 有何不同、新的合作线程模型如何工作,以及如何确保您的 app 获得最佳性能。为了充分了解本节内容,我们建议先观看“认识 Swift 中的 async/await”、“探索 Swift 中的结构化并发”和“保护 Swift 参与者的可变状态”。
资源
相关视频
WWDC23
WWDC22
WWDC21
WWDC17
WWDC16
-
下载
音乐 (幕后的Swift并发) 大家好 欢迎观看 “幕后的Swift并发” 我叫罗奇尼 在达尔文运行时团队工作 今天 我和我的同事瓦伦 非常高兴与您谈论 有关Swift并发的 一些潜在细微差别
这是一个高级讲座 建立在 一些往期关于Swift 并发的讲座之上 如果您不熟悉异步执行/异步等待 结构化并发和执行元的概念 我建议您先观看其他人的讲座 在之前关于Swift并发的讨论中 您已经了解了今年Swift的 各种语言特性以及如何使用它们 在本次讲座中 我们将更深入地了解 这些原语的设计方式 不仅是为了语言安全 也是为了性能和效率 当您在自己的App中尝试 并采用Swift并发时 我们希望本次讲座将为您 提供能够更好推理Swift并发的 思维模式 以及它如何与现有的线程库 (如Grand Central Dispatch)连接 我们今天要讨论几件事 首先 我们将讨论Swift并发 背后的线程模型 并将其与 Grand Central Dispatch进行对比 我们将讨论如何利用 并发语言特性 为Swift创建新的线程池 从而实现更好的性能和效率 最后 在本节中 我们将谈及移植代码 使用Swift并发时需要 牢记的注意事项 然后 瓦伦将通过执行元 谈论Swift并发中的同步 我们将讨论执行元是 如何在幕后工作的 它们与您可能已经 熟悉的现有同步原语 (如串行调度队列)相比如何 最后 在使用执行元编写代码时 要记住一些事情 今天有很多内容要讲 所以我们立刻开始吧 在今天关于线程模型的讨论中 我们首先要看一个 使用目前可用技术编写的示例App 如Grand Central Dispatch 然后 我们将看到当用 Swift并发重写时 同样的App会被优化到什么程度 假设我想编写自己的 动态资讯阅读器App 让我们谈谈我的App的高级组件 可能是什么 我的App将有一个 驱动用户界面的主线程 我将创建一个 跟踪用户所订阅新闻提要的数据库 最后创建一个子系统 用来处理网络逻辑 从提要中获取最新的内容 让我们考虑如何用 Grand Central Dispatch队列 构建这个App 假设用户要求查看最新消息 在主线程上 我们将 处理用户手势事件 从这里 我们将把请求 dispatch_async() 到一个处理数据库 操作的串行队列中 其原因是双重的 首先 通过将工作分派到不同的队列 我们确保主线程即使 在等待潜在的大量工作发生时 也能保持对用户输入的响应 其次 对数据库的访问受到保护 因为串行队列保证了互斥 在数据库队列中 我们将遍历用户订阅的新闻提要 并为每个提要安排一个 URLSession网络请求 来下载该提要的内容 随着网络请求结果的到来 URLSession回调将在 并发队列 delegateQueue上 被调用 在每个结果的完成处理程序中 我们将使用来自每个提要的最新请求 同步更新数据库 以便缓存它供将来使用 最后 我们将唤醒主线程 来刷新用户界面 这似乎是构建这样一个App的 一种非常合理的方式 我们已经确保在处理请求时 不阻塞主线程 通过并发处理网络请求 我们利用了程序中 固有的并行性 让我们仔细看看一个代码片段 它展示了我们如何处理 网络请求的结果 首先 我们创建了 一个URLSession 用于从新闻提要执行下载 正如您所看到的 我们已经将 这个URLSession的 delegateQueue设置为 concurrentQueue 然后 我们遍历所有需要 更新的新闻提要 并为每个提要在 URLSession中安排 一个dataTask 在dataTask (将在delegateQueue 上调用)的完成处理程序中 我们将下载的结果反序列化 并将其格式化为文章 然后 在更新提要的结果之前 我们根据数据库队列执行 dispatch_sync() 在这里 您可以看到我们已经编写了 一些直线代码来完成 一些相当简单的工作 但是这些代码有一些 隐藏的性能缺陷 为了更好地理解这些性能问题 我们需要首先深入研究线程是 如何处理GCD队列工作的 在Grand Central Dispatch中 当工作被排入队列时 系统将启动一个线程 来服务该工作项 因为一个并发队列 可以同时处理多个工作项 所以系统会启动几个线程 直到我们使所有的CPU内核饱和 然而 如果一个线程阻塞了—— 就像这里第一个 CPU内核上看到的这样—— 并且在并发队列上有更多的工作要做 那么GCD将会调出更多的线程 来排出剩余的工作项 其原因是双重的 首先 通过给进程另一个线程 我们能够确保每个内核 继续有一个线程在任何给定的时间 执行工作 这为App提供了 一个良好且持续的并发级别 其次 被阻塞的线程 可能正在等待资源 比如信号 然后才能进行下一步的工作 被启动以继续处理队列的新线程 可能能够帮助 第一个线程 正在等待的资源解除阻塞 现在我们对GCD中的线程 带来的好处有了更多的了解 让我们回头看看新闻App中代码的 CPU执行情况 在像Apple Watch 这样的双核设备上 GCD将首先调出两个线程 来处理提要更新结果 随着线程阻塞对数据库队列的访问 会创建更多的线程来继续处理 网络队列 然后 CPU必须在处理 网络结果的不同线程之间 进行上下文切换 如各个线程之间的 白色垂直线所示 这意味着 在我们的新闻App中 最终很容易产生大量的线程 如果用户有100个提要需要更新 那么当网络请求完成时 这些URL数据任务中的每一个 都会在并发队列中创建完成块 当数据库队列中的每个回调阻塞时 GCD会引发更多的线程 导致App的线程很多 现在您可能会问 App线程很多时有什么不好? App中拥有大量线程 意味着系统的线程数超过了 我们的CPU内核数 试想一下 一部拥有六个 CPU内核的iPhone 如果我们的新闻App有100个 提要更新需要处理 这意味着该iPhone的线程数 比内核数多16倍 这就是我们所说的 “线程爆炸”现象 我们之前的一些WWDC讲座 已经深入讨论了与此相关的风险 包括App中出现死锁的可能性 线程爆炸还会带来 可能不会立即显现出来的 内存与调度开销 所以让我们进一步研究一下 回顾一下我们的新闻App 每个被阻塞的线程都 在等待再次运行时 保留着宝贵的内存和资源 每个被阻塞的线程都有一个堆栈 和相关的内核数据结构来跟踪线程 其中一些线程可能持有其他 正在运行的线程可能需要的锁 对于没有进展的线程来说 这是需要保留的大量资源和内存 线程爆炸还会导致 更大的调度开销 随着新线程的创建 CPU需要执行完整的 线程上下文切换 以便从旧线程切换到 开始执行新线程 当被阻塞的线程再次变得可运行时 调度程序将不得不分时 处理CPU上的线程 以便它们都能够向前推进 现在 如果这种情况发生几次的话—— 线程的分时会处理很好 这就是并发的力量 但是当出现线程爆炸时 不得不在内核有限的设备上分时处理 数百个线程会导致过多的上下文切换 这些线程的调度延迟 超过了它们将做的有用工作的数量 从而导致CPU 运行效率也较低 正如我们到目前为止所看到的 在编写带有GCD队列的App时 很容易忽略线程卫生 的一些细微差别 从而导致性能不佳 和更大的开销 在此经验的基础上 Swift在将并发设计到语言中时 采取了不同的方法 我们在构建Swift并发时 也考虑了性能与效率 因此App可以实现受控 结构化与安全的并发 借助Swift 我们希望 将App的执行模式 从以下具有大量线程和 上下文切换的模式更改为这种模式 您可以看到 这里只有两个线程 在我们的双核系统上执行 没有线程上下文切换 所有被阻塞的线程都消失了 取而代之的是一个 轻量级的对象 称为延续 用于跟踪工作的恢复 当线程在Swift并发 下执行工作时 它们会在延续之间切换 而不是执行完整的线程上下文切换 这意味着我们现在只付出了 函数调用的代价 因此 我们希望Swift 并发的运行时行为 是只创建与CPU内核一样多的线程 并且当线程被阻塞时 能够在工作项之间廉价而高效地切换 我们希望您能够编写 易于推理的直线代码 并为您提供安全 可控的并发性 为了实现我们所追求的这种行为 操作系统需要一个线程 不会阻塞的运行时契约 而这只有在语言能够提供给 我们的情况下才有可能 因此 Swift的并发模型 和围绕它的语义 就是基于这个目标而设计的 为此 我想深入探讨Swift的 两个语言级特性 它们使我们能够 与运行时保持契约 第一个来自await的语义 第二个来自Swift运行时对任务 依赖关系的跟踪 让我们在示例新闻App的上下文中 考虑这些语言特性 这是我们之前讨论过的处理新闻提要 更新结果的代码片段 让我们看看当用Swift并发原语 编写时 这个逻辑是什么样子的 我们首先从创建助手函数的异步 实现开始 然后 我们不再在并发调度队列中 处理网络请求的结果 而是使用任务组 来管理并发性 在任务组中 我们将为每个 需要更新的提要创建子任务 每个子任务将使用共享的 URLSession从提要的 URL执行下载 然后 它将反序列化下载的结果 将它们格式化为文章 最后 我们将调用异步函数来更新数据库 这里 当调用任何异步函数时 我们用一个await关键字 对其进行标注 从“在Swift中遇到async /await”的讲座中 我们了解到await是异步等待 也就是说 在等待 异步函数的结果时 它不会阻塞当前线程 相反 该函数可能被挂起 线程将被释放以执行其他任务 这是怎么发生的? 如何放弃线程呢? 我的同事瓦伦马上将 阐明在Swift运行时中 是如何实现这一点的 谢谢 罗奇尼 在开始介绍异步函数 是如何实现的之前 让我们快速复习一下 非异步函数是如何工作的 运行程序中的每个 线程都有一个堆栈 用于存储函数调用的状态 现在让我们只关注这一个线程 当线程执行一个函数调用时 一个新的帧被推送到它的堆栈上 这个新创建的堆栈帧可以被函数 用来存储局部变量 返回地址 和任何其他需要的信息 一旦函数完成执行并返回 它的堆栈帧就会弹出 现在我们来想想异步函数 假设一个线程从 updateDatabase 函数调用了提要类型的add方法 在此阶段 最新的 堆栈帧将用于“add” 堆栈帧存储不需要 跨挂起点可用的局部变量 add的主体有一个挂起点 标记为await 局部变量 ID和article 在定义后会立即在for 循环的主体中使用 中间没有任何挂起点 因此它们将被存储在堆栈帧中 此外 堆上将有两个异步帧 一个 用于updateDatabase 一个用于add 异步帧存储需要跨挂起点 可用的信息 请注意 newArticles 参数是在await之前定义的 但需要在await之后可用 这意味着用于add的异步帧 将跟踪newArticles 假设线程继续执行 当save函数开始执行时 用于add的堆栈帧被用于 save的堆栈帧替换 不是添加新的堆栈帧 而是替换最上面的堆栈帧 因为将来需要的任何变量都 已经存储在异步帧列表中 save函数还获得一个 异步帧供其使用 当文章被保存到数据库时 如果线程可以做一些有用的工作 而不是被阻塞 那就更好了 假设save函数的执行被挂起 并且线程被重新使用 去做一些其他有用的工作 而不是被阻塞 由于跨挂起点维护的所有信息 都存储在堆上 因此可以在稍后阶段继续执行 这个异步帧列表是延续部分的 运行时表示 假设过了一会儿 数据库请求完成 假设释放了一些线程 可能是与之前相同的线程 也可能是不同的线程 假设save函数在这个线程上 继续执行 一旦它完成执行并返回一些ID 那么用于save的堆栈帧将再次 被用于add的堆栈帧所取代 之后 线程可以开始执行zip 压缩两个数组是一个同步的操作 因此它会创建一个新的堆栈帧 由于Swift继续使用 操作系统堆栈 异步和非异步Swift代码都可以 有效地调用C和 Objective-C 此外 C和Objective-C 代码可以继续 有效地调用非异步Swift代码 一旦zip函数完成 它的堆栈帧将被弹出 执行将继续 到目前为止 我已经描述了 await是如何设计 以确保有效的挂起和恢复 同时释放线程的资源来做其他工作的 接下来 罗奇尼将描述第二种语言特性 即运行时对任务之间依赖关系的跟踪 谢谢 瓦伦 如前所述 一个函数可以 在一个await点 (也称为潜在挂起点) 分解成多个延续 在这种情况下 URLSession数据 任务是异步函数 它之后的剩余工作是延续 只有在异步函数完成后 才能执行延续 这是由Swift并发运行时 跟踪的依赖关系 类似地 在任务组中 父任务可以创建几个子任务 并且在父任务可以继续之前 这些子任务中的每一个都需要完成 这是一种以代码实现的依赖关系 由任务组的范围来表示 因此Swift编译器和 运行时明确知道 在Swift中 任务只能等待 Swift运行时已知 的其他任务—— 无论是延续还是子任务 因此 使用Swift的并发原语 构建的代码为运行时 提供了对任务之间 依赖链的清晰理解 到目前为止 您已经 了解了Swift的语言功能 如何允许任务在await期间挂起 相反 正在执行的线程能够推理 任务依赖关系 并选择不同的任务 这意味着用Swift 并发编写的代码 可以维护一个 线程总是能够向前推进的运行时契约 我们利用这个运行时契约 来构建对Swift并发 的集成OS支持 以一个新的协作线程池的形式 来支持Swift并发 作为默认执行器 新的线程池将只产生 与CPU内核数量相等的线程 从而确保不会过度使用系统 与工作项阻塞时就会产生更多线程的 GCD并发队列不同 Swift线程总是可以向前推进 因此 默认运行时可以明智地 控制产生多少线程 这使我们能够为您的App 提供所需的并发性 同时确保避免 已知的过度并发的隐患 在之前WWDC关于Grand Central Dispatch 并发性的讲座中 我们建议 您将App组织成不同的子系统 并为每个子系统维护 一个串行调度队列 来控制App的并发性 这意味着 如果不冒线程爆炸的风险 您很难在一个子系统中 获得大于1的并发性 有了Swift 该语言为我们提供 运行时利用的强大不变量 从而能够在默认运行时透明地 为您提供更好 控制的并发性 现在 您对Swift并发的线程 模型有了更多的了解 我们来整理整理在代码中采用这些 令人兴奋的新特性时 一些需要记住的事项 您需要记住的第一个考虑因素 与将同步代码转换为异步代码时 的性能有关 前面 我们讨论了与 并发相关的一些成本 例如Swift运行时中的额外 内存分配和逻辑 因此 当在代码中引入并发的成本 超过管理它的成本时 您需要小心只使用Swift 并发来编写新代码 这里的代码片段可能实际上并没有 从额外的并发中获益 这种并发仅仅是 为了从用户的默认值中读取 一个值而产生一个子任务 这是因为子任务完成的有用工作 会因创建和管理任务的成本而减少 因此 我们建议在 您采用Swift并发时 使用仪器系统跟踪 来分析您的代码 以了解其性能特征 第二件需要注意的事情是 await周围原子性的概念 Swift不保证在await之前 执行代码的线程也是 获得延续的线程 事实上 await是 代码中的一个显式点 它表明原子性被破坏了 因为任务可能会被自动地取消调度 因此 您应该注意不要在 await中持有锁 同样 线程私有数据也不会 在await期间保留 代码中任何期望线程局部性的假设 都应该重新考虑 以解释await 的挂起行为 最后要考虑的是 运行时契约 它是Swift中 高效线程模型的基础 回想一下 借助Swift 该语言允许我们 维护一个运行时契约 即线程将总是能够向前推进 正是基于该契约 我们建立了一个协作线程池 作为Swift的默认执行器 当您采用Swift并发时 确保您在代码中继续维护 这个契约是很重要的 这样协作线程池才能以最佳方式运行 通过使用使代码中的依赖关系 显式且已知的安全原语 可以在协作线程池中维护这个契约 有了像await 执行元和任务组 这样的Swift并发原语 这些依赖关系在编译时就可以知道了 因此 Swift编译器会 强制执行这一点 并帮助您保留运行时契约 像os_unfair_locks 和NSLocks这样的原语也是 安全的 但是在使用它们时需要谨慎 当用于围绕一个紧密的 众所周知的关键部分的数据同步时 在同步代码中 使用锁是安全的 这是因为持有锁的线程总是 能够朝着释放 锁的方向前进 因此 尽管原语可能会在短时间内 阻塞一个处于争用状态的线程 但它不会违反前进的 运行时契约 值得注意的是 与Swift并发原语不同 没有编译器支持来帮助正确使用锁 因此您有责任正确使用该原语 另一方面 像信号量和条件变量 这样的原语与Swift并发 一起使用是不安全的 这是因为它们对Swift运行时 隐藏了依赖信息 但是在代码的执行中引入了依赖 因为运行时不知道这种依赖性 所以它不能做出正确的调度决策 并解决它们 特别是 不要使用创建 非结构化任务的原语 然后通过使用信号量 或不安全原语追溯性地引入 跨任务边界的依赖关系 这样的代码模式意味着一个线程可以 无限期地阻塞信号量 直到另一个线程能够解除阻塞 这违反了线程前进的 运行时契约 为了帮助您识别代码库中 这种不安全原语的使用 我们建议 使用以下环境变量测试App 这将在修改后的调试 运行时下运行App 将强制执行向前进度的不变量 此环境变量可以在项目方案的 “运行参数”窗格 的Xcode中设置 如下所示 使用此环境变量运行App时 如果您看到协作线程池中的线程 似乎挂起 则表明使用了不安全的阻塞原语 现在 已经了解了线程模型是如何 为Swift并发设计的 让我们进一步了解一下在这个 新世界中可用于同步 状态的原语 在关于执行元的 Swift并发讨论中 您已经看到了如何使用执行元 来保护可变状态免受并发访问 换句话说 执行元提供了 一个强大的新同步原语 您可以使用它 忆及执行元保证互斥; 一个执行元一次最多只能 执行一个方法调用 互斥意味着执行元的状态 不会被并发访问 从而防止数据竞争 让我们看看执行元如何与 其他形式的互斥进行比较 考虑前面的例子 通过同步到一个串行队列 用一些文章更新数据库 如果队列还没有运行 则我们就说没有争用 在这种情况下 调用线程被重新用来执行 队列中的新工作项 无需任何上下文切换 相反 如果串行队列已经在运行 则称该队列处于争用状态 在这种情况下 调用线程被阻塞 正如罗奇尼在前面的讲座中所描述的 这种阻塞行为触发了线程爆炸 锁也有同样的行为 由于与阻塞相关的问题 我们通常建议 您使用调度异步 调度异步的主要好处是 它是非阻塞的 所以即使在争用下 也不会导致线程爆炸 将调度异步与串行队列 一起使用的缺点是 当没有争用时 调度需要请求一个新线程 来做异步工作 而调用线程继续做其他事情 因此 调度异步的频繁使用会导致 过多的线程唤醒和上下文切换 这就把我们带到了执行元 Swift的执行元通过利用 协作线程池进行高效调度 将两者的优点结合在一起 当您在未运行的执行元上调用方法时 可以重用调用线程来执行方法调用 在被调用的执行元 已经在运行的情况下 调用线程可以挂起 它正在执行的函数 并开始其他工作 让我们看看这两个属性 在示例新闻App的 上下文中是如何工作的 让我们关注数据库和网络子系统 当更新App以使用 Swift并发时 数据库的串行队列将 被数据库执行元替换 网络的并发队列可以由 每个新闻提要的一个执行元代替 为了简单起见 我在这里 只展示了三个提要执行元—— 运动提要 天气提要 和健康提要—— 但实际上 还会有更多 这些执行元将在协作线程池中运行 提要执行元与数据库交互 以保存文章并执行其他操作 这种交互涉及到从一个执行元 到另一个执行元的执行切换 我们称这个过程为“执行元跳转” 让我们讨论一下执行元 跳转是如何工作的 假设体育提要的执行元 运行在协作池中的一个线程上 它决定将一些文章保存到数据库中 现在 让我们考虑数据库没有被使用 这是无争用的情况 线程可以直接从体育提要执行元 跳转到数据库执行元 这里有两件事需要注意 首先 线程在跳转执行元时没有阻塞 第二 跳转不需要不同的线程; 运行时可以直接挂起 运动提要执行元的工作项 并为数据库执行元 创建一个新的工作项 假设数据库执行元运行了一段时间 但是它没有完成第一个工作项 此时 假设天气提要执行元 试图在数据库中保存一些文章 这将为数据库执行元 创建一个新的工作项 执行元通过保证互斥来确保安全; 在给定时间 最多 有一个工作项是活动的 由于已经有一个活动的工作项D1 新的工作项D2将保持待处理状态 执行元也是非阻塞的 在这种情况下 天气提要执行元将被挂起 它正在执行的线程现在被释放出来 做其他工作 过了一会儿 初始数据库请求完成 因此数据库执行元的 活动工作项被删除 此时 运行时可能会选择开始 为数据库执行元执行待处理工作项 或者它可以选择恢复 其中一个提要执行元 或者它可以在释放的线程上 做一些其他的工作 当有大量的异步工作 特别是大量的争用时 系统需要根据 什么工作更重要来进行权衡 理想情况下 高优先级工作 (如涉及用户交互的工作) 优先于后台工作(如保存备份) 由于可重入性的概念 执行元被 设计成允许系统优先工作 但是为了理解为什么 可重入性在这里很重要 让我们先来看看GCD 是如何处理优先级的 考虑带有串行数据库队列的 原始新闻App 假设数据库接收到 一些高优先级的工作 例如获取最新数据 来更新用户界面 也接收到了低优先级的工作 例如将数据库备份到iCloud 这需要在某个时候完成 但不一定要立即完成 随着代码的运行 新的工作项被创建 并以某种交错的顺序 添加到数据库队列中 调度队列以严格的先进先出顺序 执行接收到的项目 不幸的是 这意味着 项目A开始执行后 五个低优先级项目需要在 下一个高优先级项目开始之前被执行 这被称为“优先级反转” 串行队列通过提高队列中 位于高优先级工作之前 的所有工作的优先级 来解决优先级反转问题 实际上 这意味着队列中的工作 会更快完成 但是 它没有解决主要问题 即项目1到5仍然需要在项目B 开始执行之前完成 解决这个问题需要改变语义模型 从严格的先进先出开始 这就引出了执行元可重入性 让我们通过一个例子来探讨可重入性 与排序之间的关系
考虑在线程上执行的数据库执行元 假设它被挂起 等待一些工作 体育提要执行元 开始在该线程上执行 假设过了一段时间 体育提要执行元调用数据库执行元 来保存一些文章 由于数据库执行元是无争用的 线程可以跳到数据库执行元 即使它有一个待处理的工作项 执行save操作 将为数据库执行元 创建一个新的工作项 这就是执行元可重入性的含义; 当执行元上的一个或 多个旧工作项被挂起时 该执行元上的新工作项 可以取得进展 执行元仍然保持互斥; 在给定的时间 最多 只能执行一个工作项 一段时间后 D2将完成执行 请注意 D2在D1 之前完成了执行 尽管它是在D1之后创建的 因此 对执行元可重入性的支持 意味着执行元可以按照 非严格先进先出的顺序执行项目 让我们重温一下之前的例子 但是使用数据库执行元 而不是串行队列 首先 工作项A将执行 因为它具有高优先级 一旦这样做了 就会和之前一样出现优先级反转 由于执行元是为可重入性而设计的 所以运行时可以选择将优先级 较高的项目移到队列的前面 在优先级较低的项目之前 这样 可以先执行 优先级较高的工作 然后执行优先级较低的工作 这直接解决了优先级反转的问题 可实现更有效的调度 和资源利用 我已经讲了一些 关于使用协作池的执行元 是如何被设计来保持互斥 和支持有效的工作优先级的内容 还有另一种执行元 主执行元 它的特征有些不同 因为它抽象了系统 中一个现有的概念: 主线程 考虑使用执行元的示例新闻App 当更新用户界面时 您将需要调用MainActor 由于主线程与协作池 中的线程不相交 这需要上下文切换 让我们通过一个代码示例来 看看这一点的性能含义 来看下面的代码 我们在 MainActor上有一个函数 updateArticles 它从数据库中加载文章 并为每篇文章更新用户界面 循环的每次迭代至少 需要两个上下文切换: 一个从主执行元跳到数据库执行元 另一个是跳回来 让我们看看这样一个 循环的CPU使用情况 由于每个循环迭代 需要两个上下文切换 因此存在一种重复模式 即两个线程在短时间内 一个接一个地运行 如果循环迭代的次数很少 并且每次迭代都有大量的工作要做 那可能没问题 然而 如果执行频繁地 在主执行元上跳和下跳 切换线程的开销就会开始增加 如果您的App在上下文切换上 花费了大量的时间 那么您应该重组您的代码 以便对主执行元的工作进行批处理 您可以通过将循环推入 loadArticles和 updateUI方法调用 来批处理工作 确保它们一次 处理数组而不是一个值 批处理工作减少了上下文切换的数量 虽然协作池中执行元之间的跳转很快 但在编写App时 您仍然需要 注意与主执行元之间的跳转 回顾之前 在这次讲座中 您已经了解了我们如何努力 使系统效率尽可能达到最高 从协作线程池的设计 (非阻塞挂起的机制) 到如何实现执行元 在每一步 我们都在使用 运行时契约的某些方面 来提高App的性能 我们很高兴看到您如何使用 这些令人难以置信的新语言功能 来编写清晰 高效和 令人愉快的Swift代码 感谢观看 祝您参会愉快 音乐
-
-
4:57 - GCD code with hidden performance pitfalls
func deserializeArticles(from data: Data) throws -> [Article] { /* ... */ } func updateDatabase(with articles: [Article], for feed: Feed) { /* ... */ } let urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: concurrentQueue) for feed in feedsToUpdate { let dataTask = urlSession.dataTask(with: feed.url) { data, response, error in // ... guard let data = data else { return } do { let articles = try deserializeArticles(from: data) databaseQueue.sync { updateDatabase(with: articles, for: feed) } } catch { /* ... */ } } dataTask.resume() }
-
13:18 - Swift concurrency equivalent using a task group
func deserializeArticles(from data: Data) throws -> [Article] { /* ... */ } func updateDatabase(with articles: [Article], for feed: Feed) async { /* ... */ } await withThrowingTaskGroup(of: [Article].self) { group in for feed in feedsToUpdate { group.async { let (data, response) = try await URLSession.shared.data(from: feed.url) // ... let articles = try deserializeArticles(from: data) await updateDatabase(with: articles, for: feed) return articles } } }
-
15:16 - Async functions: stack frames and async frames
// on Database func save(_ newArticles: [Article], for feed: Feed) async throws -> [ID] { /* ... */ } // on Feed func add(_ newArticles: [Article]) async throws { let ids = try await database.save(newArticles, for: self) for (id, article) in zip(ids, newArticles) { articles[id] = article } } func updateDatabase(with articles: [Article], for feed: Feed) async throws { // skip old articles ... try await feed.add(articles) }
-
37:13 - Excessive context switching due to Main actor hoppping
// on database actor func loadArticle(with id: ID) async throws -> Article { /* ... */ } @MainActor func updateUI(for article: Article) async { /* ... */ } @MainActor func updateArticles(for ids: [ID]) async throws { for id in ids { let article = try await database.loadArticle(with: id) await updateUI(for: article) } }
-
38:18 - Batch UI work to reduce the number of context switches
// on database actor func loadArticles(with ids: [ID]) async throws -> [Article] @MainActor func updateUI(for articles: [Article]) async @MainActor func updateArticles(for ids: [ID]) async throws { let articles = try await database.loadArticles(with: ids) await updateUI(for: articles) }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。