在本教程中,您将:
- 比较同步和异步读取非常大的文件时的速度和内存使用情况。
- 创建和使用自定义
AsyncSequence. - 创建和使用基于拉的和基于推的
AsyncStreams。
注意:这是一个中级教程。你应该熟悉“传统”并发——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为异步处理数据集合提供语言支持。有内置的AsyncSequences like NotificationCenter.Notifications,URLSession.bytes(from:delegate:) 以及它的子序列lines和characters。AsyncSequence您可以使用和AsyncIteratorProtocol或使用创建自己的自定义异步序列AsyncStream。
注意:Apple 的AsyncSequence 文档页面列出了所有内置的异步序列。
Reading a File Asynchronously
为了直接从 URL 处理数据集,URL基础类提供了自己的AsyncSequencein实现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。
要定义一个AsyncSequenceover 数据集,您需要遵循其协议并构造一个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是Actors 的序列。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 {
演员.追加(演员)
}
}
}
这是您使用此代码所做的事情:
- 您仍然创建一个
AsyncIteratorforurl.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. - 创建并使用了基于拉的和基于推的
AsyncStreams。 - 表明在
AsyncStream代码请求序列元素之前,基于拉的程序什么都不做,而基于推送的程序AsyncStream无论代码是否请求序列元素都运行。
您可以使用AsyncSequence和AsyncStream从现有代码生成异步序列——您多次调用的任何闭包,以及只报告新值且不需要返回响应的委托方法。
下载项目资料与原文地址
这里也推荐一些面试相关的内容!