在本教程中,您将:
- 比较同步和异步读取非常大的文件时的速度和内存使用情况。
- 创建和使用自定义
AsyncSequence
. - 创建和使用基于拉的和基于推的
AsyncStream
s。
注意:这是一个中级教程。你应该熟悉“传统”并发——GCD(Grand Central Dispatch)和URLSession
——以及基本的 Swift 并发特性,比如SwiftUI和SwiftUI 中的async/await和结构化并发中呈现的那些特性。
Data Files
ActorSearch的目的是帮助您通过从IMDb 数据集中搜索name.basics.tsv.gz数据集来解决询问演员姓名的难题。该文件包含一个标题行来描述每个名称的信息:
nconst
(字符串)– 姓名/人员的字母数字唯一标识符primaryName
(字符串)– 最常记入此人的姓名birthYear
– YYYY 格式deathYear
– 如果适用,采用 YYYY 格式,否则为 '\N'primaryProfession
(字符串数组)– 人的前三个职业knownForTitles
(tconsts 数组)– 此人的知名头衔
为了减少对您的网络的需求并使其易于逐行阅读,启动项目已经包含data.tsv:这是解压缩的name.basics.tsv.gz,已删除标题行。它是一个制表符分隔值 (TSV) 文件,格式为 UTF-8 字符集。
注意:不要试图通过在项目导航器中选择它来查看data.tsv。打开需要很长时间,Xcode 变得无响应。
在本教程中,您将探索将文件内容读入值数组的不同方法Actor
。data.tsv包含 11,445,101 行并且需要很长时间才能读入,因此您将仅使用它来比较内存使用情况。您将在较小的文件data-100.tsv和data-1000.tsv上尝试大部分代码,它们分别包含前 100 和 1000 行。
注意:这些文件仅在启动项目中。如果要构建和运行该项目,请将它们复制到最终项目中。
Models
打开ActorAPI.swift。Actor
是一个超级简单的结构,只有两个属性:id
和name
。
在此文件中,您将实现读取数据文件的不同方法。ActorAPI
初始化程序接受一个参数filename
并创建. url
它是一个ObservableObject
发布Actor
数组的。
starter 包含一个基本的同步方法:
func readSync () throws {
let start = Date .now
let contents = try String (contentsOf: url)
var counter = 0
contents.enumerateLines { _ , _ in
counter += 1
}
print ( " \(counter) lines" )
print ( "持续时间: \(Date.now.timeIntervalSince(start)) " )
}
String
这只是从contentsOf
文件的中创建一个url
,然后计算行数并打印该数字以及花费了多长时间。
注:enumerateLines(invoking:)
是从StringProtocol
方法桥接的NSString
方法enumerateLines(_:)
。
View
打开ContentView.swift。ContentView
创建一个ActorAPI
具有特定文件名的对象并显示该Actor
数组,并带有一个搜索字段。
searchable(text:)
首先,在闭包下面添加这个视图修饰符:
.onAppear {
做{
尝试model.readSync()
}捕捉 让错误 {
打印(error.localizedDescription)
}
}
您readSync()
在视图出现时调用,捕获并打印任何readSync()
抛出的错误。
现在,查看运行此应用程序时的内存使用情况。打开调试导航器,然后构建并运行。当仪表出现时,选择内存并观看:
在我的 Mac 上,读取这个 685MB 的文件需要 8.9 秒,并产生 1.9GB 的内存使用峰值。
接下来,您将尝试一种 Swift 并发方式来读取文件。您将遍历一个异步序列。
AsyncSequence
您Sequence
一直在使用该协议:数组、字典、字符串、范围和Data
都是序列。它们带有许多方便的方法,例如next()
、contains()
等filter()
。对序列进行循环使用其内置的迭代器,并在迭代器返回时停止nil
。
该AsyncSequence
协议的工作原理类似于Sequence
,但异步序列异步返回每个元素(呃!)。随着更多元素随着时间的推移变得可用,您可以异步迭代其元素。
- 你
await
每个元素,所以序列可以在获取或计算下一个值时暂停。 - 序列生成元素的速度可能比您的代码使用它们的速度更快:一种
AsyncStream
缓冲其值,因此您的应用程序可以在需要时读取它们。
AsyncSequence
为异步处理数据集合提供语言支持。有内置的AsyncSequence
s like NotificationCenter.Notifications
,URLSession.bytes(from:delegate:)
以及它的子序列lines
和characters
。AsyncSequence
您可以使用和AsyncIteratorProtocol
或使用创建自己的自定义异步序列AsyncStream
。
注意:Apple 的AsyncSequence 文档页面列出了所有内置的异步序列。
Reading a File Asynchronously
为了直接从 URL 处理数据集,URL
基础类提供了自己的AsyncSequence
in实现URL.lines
。这对于直接从 URL 创建异步行序列很有用。
打开ActorAPI.swift并将这个方法添加到ActorAPI
:
// 异步读取
func readAsync () async throws {
let start = Date .now
var counter = 0
for try await _ in url.lines {
计数器+= 1
}
打印(“ \(计数器)行”)
print ( "持续时间:\(Date.now.timeIntervalSince(start)) " )
}
您在异步序列上异步迭代,边走边计算行数。
这里有一些 Swift 并发魔法:url.lines
有自己的异步迭代器,for
循环调用它的next()
方法,直到序列通过返回nil
.
注意:URLSession
有一个获取异步字节序列和常用URLResponse
对象的方法。您可以检查响应状态代码,然后调用lines
此字节序列将其转换为异步行序列。
let (stream, response) = try await URLSession .shared.bytes(from: url)
guard (response as? HTTPURLResponse ) ? .statusCode == 200 else {
throw "服务器响应错误。"
}
for try await line in stream.lines {
// ...
}
Calling an Asynchronous Method From a View
要从 SwiftUI 视图调用异步方法,请使用task(priority:_:)
视图修饰符。
在ContentView
中,注释掉onAppear(perform:)
闭包并添加以下代码:
.task {
做{
尝试 等待model.readAsync()
}捕捉 让错误 {
打印(error.localizedDescription)
}
}
打开调试导航器,然后构建并运行。当仪表出现时,选择内存并观看:
在我的 Mac 上,读取文件需要 3.7 秒,内存使用量稳定在 68MB。差别很大!
在循环的每次迭代中for
,lines
序列都会从 URL 中读取更多数据。因为这是分块发生的,所以内存使用量保持不变。
Getting Actors
是时候填充actors
数组了,这样应用就可以显示一些东西了。
将此方法添加到ActorAPI
:
func getActors () async throws {
for try await line in url.lines {
let name = line.components(separatedBy: " \t " )[ 1 ]
await MainActor .run {
actor.append(演员(姓名: 姓名))
}
}
}
您无需计算行数,而是从每一行中提取名称,使用它来创建一个Actor
实例,然后将其附加到actors
. 因为actors
是 SwiftUI 视图使用的已发布属性,所以修改它必须在主队列上进行。
现在,在ContentView
闭task
包中,替换try await model.readAsync()
为:
尝试 等待model.getActors()
此外,model
使用较小的数据文件之一更新声明,data-100.tsv或data-1000.tsv:
@StateObject private var model = ActorAPI (filename: "data-100" )
构建并运行。
该列表很快出现。下拉屏幕以查看搜索字段并尝试一些搜索。使用模拟器的软件键盘 ( Command-K ) 可以更轻松地取消搜索词的首字母大写。
Custom AsyncSequence
到目前为止,您一直在使用 URL API 中内置的异步序列。您还可以创建自己的自定义AsyncSequence
,例如AsyncSequence
值Actor
。
要定义一个AsyncSequence
over 数据集,您需要遵循其协议并构造一个AsyncIterator
返回集合中数据序列的下一个元素的 an。
AsyncSequence of Actors
你需要两个结构——一个符合,AsyncSequence
另一个符合AsyncIteratorProtocol
.
在ActorAPI.swift的 outsideActorAPI
中,添加这些最小结构:
struct ActorSequence : AsyncSequence {
// 1
typealias Element = Actor
typealias AsyncIterator = ActorIterator
// 2
func makeAsyncIterator () -> ActorIterator {
return ActorIterator ()
}
}
struct ActorIterator : AsyncIteratorProtocol {
// 3
mutating func next () -> Actor ? {
返回 零
}
}
注意:如果您愿意,可以在结构内部定义迭代器结构AsyncSequence
。
以下是此代码的每个部分的作用:
- 你
AsyncSequence
生成一个Element
序列。在这种情况下,ActorSequence
是Actor
s 的序列。AsyncSequence
期望一个AsyncIterator
,你typealias
要ActorIterator
。 - 该
AsyncSequence
协议需要一个makeAsyncIterator()
方法,该方法返回一个ActorIterator
. 此方法不能包含任何异步或抛出代码。像这样的代码进入ActorIterator
. - 该
AsyncIteratorProtocol
协议需要一个next()
方法来返回下一个序列元素,或者nil
, 来表示序列的结束。
现在,要填写结构,请将这些行添加到ActorSequence
:
让文件名:字符串
让网址:网址
init(文件名:字符串){
self .filename =文件名
self .url = Bundle .main.url(forResource:文件名,withExtension:“tsv”)!
}
该序列需要文件名的参数和存储文件 URL 的属性。您在初始化程序中设置这些。
在makeAsyncIterator()
中,您将遍历url.lines
。
将这些行添加到ActorIterator
:
让url: URL
var迭代器: AsyncLineSequence < URL . 异步字节>。异步迭代器
初始化(网址:网址){
self .url = url
迭代器= url.lines.makeAsyncIterator()
}
您显式地获取了异步迭代器,url.lines
因此next()
可以调用迭代器的next()
方法。
现在,修复ActorIterator()
调用makeAsyncIterator()
:
返回 ActorIterator (url: url)
接下来,替换next()
为以下内容:
变异 func next () async -> Actor?{
do {
if let line = try await iterator.next(), ! line.isEmpty {
let name = line.components(separatedBy: " \t " )[ 1 ]
return Actor (name: name)
}
}捕捉 让错误 {
打印(error.localizedDescription)
}
返回 零
}
您将async
关键字添加到签名中,因为此方法使用异步序列迭代器。只是为了改变,你在这里处理错误而不是抛出它们。
现在,在 中ActorAPI
,修改getActors()
以使用此自定义AsyncSequence
:
func getActors () async {
for await actor in ActorSequence ( filename : filename ) {
await MainActor .run {
演员.追加(演员)
}
}
}
处理任何错误的next()
方法,因此不会抛出,并且您不必处理.ActorIterator``getActors()``try await``ActorSequence
您迭代ActorSequence(filename:)
,它返回Actor
值供您附加到actors
.
最后,在 中ContentView
,将task
闭包替换为:
.task {
等待模型.getActors()
}
代码要简单得多,现在getActors()
不会抛出。
构建并运行。
一切都一样。
AsyncStream
自定义异步序列的唯一缺点是需要创建和命名结构,这会添加到应用程序的命名空间中。AsyncStream
让您“即时”创建异步序列。
typealias
您只需使用元素类型初始化您的,而不是使用 a ,AsyncStream
然后在其尾随闭包中创建序列。
其实有两种AsyncStream
。一个有unfolding
闭包。像AsyncIterator
,它提供next
元素。它只在任务要求一个值时创建一系列值,一次一个。将其视为基于拉动或需求驱动的。
AsyncStream: Pull-based
首先,您将创建基于拉取AsyncStream
的ActorAsyncSequence
.
将此方法添加到ActorAPI
:
// AsyncStream: 基于拉取的
func pullActors () async {
// 1
var iterator = url.lines.makeAsyncIterator()
// 2
let actorStream = AsyncStream < Actor > {
// 3
do {
if let line = try await iterator.next(), ! line.isEmpty {
let name = line.components(separatedBy: " \t " )[ 1 ]
return Actor (name: name)
}
}捕捉 让错误 {
打印(error.localizedDescription)
}
返回 零
}
// 4
for await actor in actorStream {
await MainActor .run {
演员.追加(演员)
}
}
}
这是您使用此代码所做的事情:
- 您仍然创建一个
AsyncIterator
forurl.lines
。 - 然后你创建一个
AsyncStream
,指定Element
类型Actor
。 next()
并将方法的内容复制ActorIterator
到闭包中。- 现在,
actorStream
是一个异步序列,与 完全一样ActorSequence
,因此您可以像在getActors()
.
在ContentView
中,调用pullActors()
而不是getActors()
:
等待模型.pullActors()
构建并运行,然后检查它是否仍然可以正常工作。
AsyncStream: Push-based
另一种AsyncStream
有build
闭包。它创建一系列值并缓冲它们,直到有人要求它们为止。将其视为基于推送或供应驱动的。
将此方法添加到ActorAPI
:
// AsyncStream: 基于推送的
func pushActors () async {
// 1
let actorStream = AsyncStream < Actor > { continuation in
// 2
Task {
for try await line in url.lines {
let name = line.components(separatedBy: " \t " )[ 1 ]
// 3
continuation.yield( Actor (name: name))
}
// 4
延续.finish()
}
}
对于 actorStream中的等待 演员 {
await MainActor .run {
演员.追加(演员)
}
}
}
这是您在此方法中所做的事情:
- 您不需要创建迭代器。相反,您会得到一个
continuation
. build
闭包不是异步的,因此您必须在Task
异步序列上创建一个 to 循环url.lines
。- 对于每一行,您调用延续的
yield(_:)
方法将Actor
值推入缓冲区。 - 当你到达末尾时
url.lines
,你调用延续的finish()
方法。
注意:因为build
闭包不是异步的,所以你可以使用这个版本AsyncStream
来与非异步 API 交互,比如fread(_:_:_:_:)
.
在ContentView
中,调用pushActors()
而不是pullActors()
:
等待模型.pushActors()
构建并运行并确认它可以工作。
Continuations
自 Apple 首次推出 Grand Central Dispatch 以来,它就向开发人员提供了如何避免线程爆炸危险的建议。
当线程多于 CPU 时,调度程序在线程之间分时共享 CPU,执行上下文切换以换出正在运行的线程并换入阻塞的线程。每个线程都有一个堆栈和相关的内核数据结构,因此上下文切换需要时间。
当应用程序创建大量线程时——例如,当它下载成百上千张图像时——CPU 会花费太多时间进行上下文切换,而没有足够的时间做有用的工作。
在 Swift 并发系统中,线程的数量最多只有 CPU 的数量。
当线程在 Swift 并发下执行工作时,系统使用一个称为continuation的轻量级对象来跟踪在何处恢复暂停任务的工作。在任务延续之间切换比执行线程上下文切换更便宜、更有效。
注意:这个带有延续的线程图像来自 WWDC21 Session 10254。
当任务挂起时,它会继续捕获其状态。它的线程可以恢复另一个任务的执行,从它挂起时创建的延续中重新创建它的状态。这样做的代价是函数调用。
async
当您使用函数时,这一切都发生在幕后。
但是您也可以继续手动恢复执行。的缓冲形式AsyncStream
使用延续来yield
流式传输元素。
不同的延续 API 可帮助您重用现有代码,如完成处理程序和委托方法。要了解如何操作,请查看Swift 中的现代并发,第 5 章,“中间 async/await 和 CheckedContinuation”。
Push or Pull?
Push-based 就像工厂制造衣服并将它们存储在仓库或商店中,直到有人购买它们。Pull-based 就像从裁缝那里订购衣服。
在基于拉取和基于推送之间进行选择时,请考虑与您的用例的潜在不匹配:
- 基于拉的(展开)
AsyncStream
:您的代码需要比异步序列更快的值。 - 基于推送(缓冲)
AsyncStream
:异步序列生成元素的速度比您的代码读取它们的速度更快,或者以不规则或不可预测的时间间隔生成元素,例如来自后台监视器的更新——通知、位置、自定义监视器
下载大文件时,基于拉取的方式AsyncStream
(仅在代码请求时才下载更多字节)可以让您更好地控制内存和网络使用。基于推送(AsyncStream
不暂停下载整个文件)可能会导致内存或网络使用量激增。
要了解这两种 . 的另一个区别AsyncStream
,请查看如果您的代码不使用actorStream
.
在和ActorAPI
中注释掉这段代码:pullActors()``pushActors()
对于 actorStream中的等待 演员 {
await MainActor .run {
演员.追加(演员)
}
}
接下来,在这两种方法中的这一行放置断点:
let name = line.components(separatedBy: " \t " )[ 1 ]
编辑两个断点以记录断点名称和命中计数,然后继续:
现在,在 中ContentView
,设置task
为调用pullActors()
:
.task {
等待模型.pullActors()
}
构建并运行,然后打开调试控制台:
actorStream
不会出现日志消息,因为当您的代码不要求其元素时,基于拉取的代码不会运行。除非您要求下一个元素,否则它不会从文件中读取。
现在,切换task
到调用pushActors()
:
.task {
等待模型.pushActors()
}
构建并运行,打开调试控制台:
actorStream
即使您的代码不要求任何元素,基于推送的也会运行。它读取整个文件并缓冲序列元素。
Conclusion
注意:数据文件仅在启动项目中。如果要构建和运行该项目,请将它们复制到最终项目中。
在本教程中,您:
- 比较了同步和异步读取非常大的文件时的速度和内存使用情况。
- 创建并使用了自定义
AsyncSequence
. - 创建并使用了基于拉的和基于推的
AsyncStream
s。 - 表明在
AsyncStream
代码请求序列元素之前,基于拉的程序什么都不做,而基于推送的程序AsyncStream
无论代码是否请求序列元素都运行。
您可以使用AsyncSequence
和AsyncStream
从现有代码生成异步序列——您多次调用的任何闭包,以及只报告新值且不需要返回响应的委托方法。
下载项目资料与原文地址
这里也推荐一些面试相关的内容!