이 자습서에서는 다음을 수행합니다.
- 매우 큰 파일을 동기식 및 비동기식으로 읽을 때 속도와 메모리 사용량을 비교합니다.
- 사용자 지정을 만들고 사용
AsyncSequence
합니다. - 풀 기반 및 푸시 기반
AsyncStream
s를 만들고 사용합니다.
참고 : 이것은 중급 수준의 튜토리얼입니다. "전통적인" 동시성 - GCD(Grand Central Dispatch) 및 URLSession
- SwiftUI 및 구조적 동시성 및 구조적 동시성에서 SwiftUI 및 async/await에서 제공되는 것과 같은 기본 Swift 동시성 기능에 익숙해야 합니다.
데이터 파일
ActorSearch 의 목적은 IMDb 데이터 세트에서 name.basics.tsv.gz 데이터 세트를 검색하여 배우의 이름을 묻는 어려운 문제 를 해결하는 데 도움이 되는 것 입니다. 파일에는 각 이름에 대한 정보를 설명하는 헤더 행이 포함되어 있습니다.
nconst
(문자열) – 이름/사람에 대한 영숫자 고유 식별자primaryName
(문자열) – 가장 자주 기록되는 사람의 이름birthYear
– YYYY 형식deathYear
– 해당하는 경우 YYYY 형식으로, 그렇지 않으면 '\N'primaryProfession
(문자열 배열) – 개인의 처음 세 가지 직업knownForTitles
(tconst 배열) – 사람의 잘 알려진 직함
네트워크에 대한 요구를 줄이고 한 줄씩 더 쉽게 읽을 수 있도록 시작 프로젝트에는 이미 data.tsv 가 포함되어 있습니다. 이것은 헤더 줄이 제거된 압축을 푼 name.basics.tsv.gz 입니다. UTF-8 문자 집합의 탭으로 구분된 값(TSV) 파일입니다.
참고 : 프로젝트 탐색기에서 data.tsv 를 선택하여 보려고 하지 마십시오 . 여는 데 시간이 오래 걸리고 Xcode가 응답하지 않습니다.
이 자습서에서는 파일 내용을 값 배열로 읽는 다양한 방법을 탐색합니다 Actor
. data.tsv 는 11,445,101줄을 포함하고 있으며 읽는 데 오랜 시간이 걸리므로 메모리 사용량을 비교하는 데만 사용합니다. 처음 100행과 1000행을 각각 포함하는 더 작은 파일 data-100.tsv 및 data-1000.tsv 에서 대부분의 코드를 시도할 것입니다.
참고 : 이 파일은 시작 프로젝트에만 있습니다. 프로젝트를 빌드하고 실행하려면 최종 프로젝트에 복사합니다.
모델
ActorAPI.swift 를 엽니 다 . 및 Actor
의 두 가지 속성만 있는 매우 간단한 구조입니다 .id
name
이 파일에서는 데이터 파일을 읽는 다양한 방법을 구현합니다. ActorAPI
이니셜라이저는 하나의 매개변수 를 가져 와서 filename
생성하며 .url
ObservableObject
Actor
스타터에는 기본 동기화 방법이 포함되어 있습니다.
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(_:)
보다
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
从现有代码生成异步序列——您多次调用的任何闭包,以及只报告新值且不需要返回响应的委托方法。
下载项目资料与原文地址
这里也推荐一些面试相关的内容!