AsyncSequence and AsyncStream Tutorial for iOS

In this tutorial, you will:

  • Compare speed and memory usage when reading very large files synchronously and asynchronously.
  • Create and use custom AsyncSequence.
  • Create and use pull-based and push-based AsyncStreams.

NOTE : This is an intermediate level tutorial. You should be familiar with "traditional" concurrency - GCD (Grand Central Dispatch) and URLSession- and basic Swift concurrency features such as those presented in SwiftUI and async/await in SwiftUI and Structured Concurrency.

Data Files

The purpose of ActorSearch is to help you solve the difficult problem of asking for an actor's name by searching the name.basics.tsv.gz dataset from the IMDb dataset . The file contains a header line to describe information about each name:

  • nconst(string) – Alphanumeric unique identifier for the name/person
  • primaryName(string) – The person's name most often credited
  • birthYear– YYYY format
  • deathYear– If applicable, in YYYY format, otherwise '\N'
  • primaryProfession(array of strings) – Person’s first three occupations
  • knownForTitles(array of tconsts) – the well-known title of the person

To reduce the demands on your network and make it easier to read line by line, the startup project already contains data.tsv : this is the unpacked name.basics.tsv.gz , with the header line removed. It is a tab-separated value (TSV) file in the UTF-8 character set.

Note : Do not try to view data.tsv by selecting it in the project navigator . It takes a long time to open and Xcode becomes unresponsive.

In this tutorial, you will explore different ways to read the contents of a file into an array of values Actor. data.tsv contains 11,445,101 lines and takes a long time to read in, so you'll only use that to compare memory usage. You will try most of the code on the smaller files data-100.tsv and data-1000.tsv , which contain the first 100 and 1000 lines respectively.

Note : These files are only in the startup project. If you want to build and run the project, copy them into the final project.

Models

Open ActorAPI.swift . Actoris a super simple structure with only two properties: idand name.

In this file, you will implement different methods for reading data files. ActorAPIThe initializer takes one parameter filenameand creates it.  urlIt's an array that ObservableObjectpublishes .Actor

starter contains a basic synchronization method:

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)) " )
}

StringThis just contentsOfcreates one from the file url, then counts the lines and prints that number and how long it took.

Note : enumerateLines(invoking:)is a StringProtocolmethod bridged from a NSStringmethodenumerateLines(_:) .

View

Open ContentView.swift . ContentViewCreates an ActorAPIobject with a specific filename and displays the Actorarray, with a search field.

searchable(text:)First, add this view modifier below the closure:

.onAppear {
  做{
    尝试model.readSync()
  }捕捉 让错误 {
    打印(error.localizedDescription)
  }
}

readSync()在视图出现时调用,捕获并打印任何readSync()抛出的错误。

现在,查看运行此应用程序时的内存使用情况。打开调试导航器,然后构建并运行。当仪表出现时,选择内存并观看:

image.png

在我的 Mac 上,读取这个 685MB 的文件需要 8.9 秒,并产生 1.9GB 的内存使用峰值。

接下来,您将尝试一种 Swift 并发方式来读取文件。您将遍历一个异步序列

AsyncSequence

Sequence一直在使用该协议:数组、字典、字符串、范围和Data都是序列。它们带有许多方便的方法,例如next()contains()filter()。对序列进行循环使用其内置的迭代器,并在迭代器返回时停止nil

AsyncSequence协议的工作原理类似于Sequence,但异步序列异步返回每个元素(呃!)。随着更多元素随着时间的推移变得可用,您可以异步迭代其元素。

  • await每个元素,所以序列可以在获取或计算下一个值时暂停。
  • 序列生成元素的速度可能比您的代码使用它们的速度更快:一种AsyncStream缓冲其值,因此您的应用程序可以在需要时读取它们。

AsyncSequence为异步处理数据集合提供语言支持。有内置的AsyncSequences like NotificationCenter.NotificationsURLSession.bytes(from:delegate:) 以及它的子序列linescharactersAsyncSequence您可以使用和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)
  }
}

打开调试导航器,然后构建并运行。当仪表出现时,选择内存并观看:

image.png

在我的 Mac 上,读取文件需要 3.7 秒,内存使用量稳定在 68MB。差别很大!

在循环的每次迭代中forlines序列都会从 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 视图使用的已发布属性,所以修改它必须在主队列上进行。

现在,在ContentViewtask包中,替换try await model.readAsync()为:

尝试 等待model.getActors()

此外,model使用较小的数据文件之一更新声明,data-100.tsvdata-1000.tsv

@StateObject  private  var model =  ActorAPI (filename: "data-100" )

构建并运行。

image.png

该列表很快出现。下拉屏幕以查看搜索字段并尝试一些搜索。使用模拟器的软件键盘 ( Command-K ) 可以更轻松地取消搜索词的首字母大写。

Custom AsyncSequence

到目前为止,您一直在使用 URL API 中内置的异步序列。您还可以创建自己的自定义AsyncSequence,例如AsyncSequenceActor

要定义一个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

以下是此代码的每个部分的作用:

  1. AsyncSequence生成一个Element序列。在这种情况下,ActorSequenceActors 的序列。AsyncSequence期望一个AsyncIterator,你typealiasActorIterator
  2. AsyncSequence协议需要一个makeAsyncIterator()方法,该方法返回一个ActorIterator. 此方法不能包含任何异步或抛出代码。像这样的代码进入ActorIterator.
  3. 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()不会抛出。

构建并运行。

image.png

一切都一样。

AsyncStream

自定义异步序列的唯一缺点是需要创建和命名结构,这会添加到应用程序的命名空间中。AsyncStream让您“即时”创建异步序列。

typealias您只需使用元素类型初始化您的,而不是使用 a ,AsyncStream然后在其尾随闭包中创建序列。

其实有两种AsyncStream。一个有unfolding闭包。像AsyncIterator,它提供next元素。它只在任务要求一个值时创建一系列值,一次一个。将其视为基于拉动或需求驱动的

AsyncStream: Pull-based

首先,您将创建基于拉取AsyncStreamActorAsyncSequence.

将此方法添加到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 {
      演员.追加(演员)
    }
  }
}

这是您使用此代码所做的事情:

  1. 您仍然创建一个AsyncIteratorfor url.lines
  2. 然后你创建一个AsyncStream,指定Element类型Actor
  3. next()并将方法的内容复制ActorIterator到闭包中。
  4. 现在,actorStream是一个异步序列,与 完全一样ActorSequence,因此您可以像在getActors().

ContentView中,调用pullActors()而不是getActors()

等待模型.pullActors()

构建并运行,然后检查它是否仍然可以正常工作。

image.png

AsyncStream: Push-based

另一种AsyncStreambuild闭包。它创建一系列值并缓冲它们,直到有人要求它们为止。将其视为基于推送或供应驱动的

将此方法添加到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 {  
      演员.追加(演员)
    }
  }
}

这是您在此方法中所做的事情:

  1. 您不需要创建迭代器。相反,您会得到一个continuation.
  2. build闭包不是异步的,因此您必须在Task异步序列上创建一个 to 循环url.lines
  3. 对于每一行,您调用延续的yield(_:)方法将Actor值推入缓冲区。
  4. 当你到达末尾时url.lines,你调用延续的finish()方法。

注意:因为build闭包不是异步的,所以你可以使用这个版本AsyncStream来与非异步 API 交互,比如fread(_:_:_:_:).

ContentView中,调用pushActors()而不是pullActors()

等待模型.pushActors()

构建并运行并确认它可以工作。

Continuations

自 Apple 首次推出 Grand Central Dispatch 以来,它就向开发人员提供了如何避免线程爆炸危险的建议。

当线程多于 CPU 时,调度程序在线程之间分时共享 CPU,执行上下文切换以换出正在运行的线程并换入阻塞的线程。每个线程都有一个堆栈和相关的内核数据结构,因此上下文切换需要时间。

当应用程序创建大量线程时——例如,当它下载成百上千张图像时——CPU 会花费太多时间进行上下文切换,而没有足够的时间做有用的工作。

在 Swift 并发系统中,线程的数量最多只有 CPU 的数量。

当线程在 Swift 并发下执行工作时,系统使用一个称为continuation的轻量级对象来跟踪在何处恢复暂停任务的工作。在任务延续之间切换比执行线程上下文切换更便宜、更有效。

image.png

注意:这个带有延续的线程图像来自 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 ]

编辑两个断点以记录断点名称和命中计数,然后继续:

image.png 现在,在 中ContentView,设置task为调用pullActors()

.task {
  等待模型.pullActors()
}

构建并运行,然后打开调试控制台:

image.png

actorStream不会出现日志消息,因为当您的代码不要求其元素时,基于拉取的代码不会运行。除非您要求下一个元素,否则它不会从文件中读取。

现在,切换task到调用pushActors()

.task {
  等待模型.pushActors()
}

构建并运行,打开调试控制台:

image.png

actorStream即使您的代码不要求任何元素,基于推送的也会运行。它读取整个文件并缓冲序列元素。

Conclusion

注意:数据文件仅在启动项目中。如果要构建和运行该项目,请将它们复制到最终项目中。

在本教程中,您:

  • 比较了同步和异步读取非常大的文件时的速度和内存使用情况。
  • 创建并使用了自定义AsyncSequence.
  • 创建并使用了基于拉的和基于推的AsyncStreams。
  • 表明在AsyncStream代码请求序列元素之前,基于拉的程序什么都不做,而基于推送的程序AsyncStream无论代码是否请求序列元素都运行。

您可以使用AsyncSequenceAsyncStream从现有代码生成异步序列——您多次调用的任何闭包,以及只报告新值且不需要返回响应的委托方法。

下载项目资料与原文地址

这里也推荐一些面试相关的内容!

Guess you like

Origin juejin.im/post/7116409948665282568