WWDC Quick Look 💓 By SwiftGGTeam
Meet Swift Async Algorithms

Meet Swift Async Algorithms

元の動画を見る

ハイライト

Swift Async Algorithms は、AsyncSequence の Zip、Merge、Debounce、Chunk などの高度なアルゴリズムを提供するオープン ソース パッケージです。また、Swift 5.7 の Clock API を使用して、時間ベースのフロー制御を実装します。

主要内容

コールバックから AsyncSequence へ

インスタント メッセージング アプリでは、現在、メッセージの到着はコールバックによって行われます。各アカウントには独自のメッセージ コールバックがあり、複数のアカウントがある場合は手動で調整する必要があります。コードにはネストされた完了ハンドラーが多数含まれており、状態管理はますます複雑になります。

(01:15) Swift の AsyncSequence は、非同期値ストリーミングをfor-await-inトラバースするシーケンス。 Swift Async Algorithms パッケージは、これに基づいてより高度なアルゴリズムを提供します。

struct Account {
  var messages: AsyncStream<Message>
}

actor AccountManager {
  var primaryAccount: Account
  var secondaryAccount: Account?
}

protocol MessagePreview {
  func displayPreviews(_ manager: AccountManager) async
}

キーポイント:

  • AsyncStreamコールバックを AsyncSequence に変換し、メッセージの順序を維持する
  • for-await-in構文により非同期トラバーサルが同期コードのように見える

Zip: 複数の非同期結果を同時に待機します

(02:16) ユーザーがビデオを送信するときは、プレビューの生成とトランスコードを同時に行う必要があります。 2 つの操作は独立して実行されますが、アップロード時にはペアで行う必要があります。

以前のアプローチは使用することでしたTaskGroupまたはasync let手動調整。 Zip アルゴリズムはこの問題を直接解決します。

// upload attachments of videos and previews such that every video has a preview
// that are created concurrently so that neither blocks each other.

for try await (vid, preview) in zip(videos, previews) {
  try await upload(vid, preview)
}

キーポイント:

  • zip2 つの AsyncSequence を同時に走査してタプルを生成する
  • どちらかの当事者が先に完了した場合、もう一方の当事者を待ち、ブロックしません。
  • いずれかのシーケンスでエラーがスローされた場合、Zip 全体が再スローされます。

マージ: 複数のメッセージ フローをマージします。

(04:04) アプリは複数のアカウントをサポートしており、各アカウントには独自のメッセージ フローがあります。ユーザーは、どのアカウントからのメッセージであっても、すべてのメッセージを 1 つのインターフェイスで確認したいと考えています。

Merge アルゴリズムは、同じタイプの複数の AsyncSequence を 1 つに結合します。

// Display previews of messages from either the primary or secondary account

for try await message in merge(primaryAccount.messages, secondaryAccount.messages) {
  displayPreview(message)
}

キーポイント:

  • mergeすべての入力シーケンスに同じタイプの要素が必要です
  • 出力順序は、どのシーケンスが最初に値を生成するかによって異なります。
  • いずれかのシーケンスでエラーが発生した場合、他のシーケンスの繰り返しはキャンセルされます。

Clock API: 時間を操作する

(05:43) 非同期コードで時間を扱うのは常に面倒でした。DispatchQueue.asyncAfter柔軟性が足りず、Timerライフサイクルを手動で管理する必要があります。

Swift 5.7 では、Clock、Instant、Duration の 3 つの時間プリミティブが導入されています。

// Sleep until a given deadline

let clock = SuspendingClock()
var deadline = clock.now + .seconds(3)
try await clock.sleep(until: deadline)

キーポイント:

  • Clockプロトコルの定義nowそしてsleep(until:)2つのプリミティブ
  • Durationサポート.seconds().milliseconds()などの施工方法

2 つの組み込みクロックは異なる動作をします。

let clock = SuspendingClock()
let elapsed = await clock.measure {
  await someLongRunningWork()
}
// Elapsed time reads 00:05.40

let clock = ContinuousClock()
let elapsed = await clock.measure {
  await someLongRunningWork()
}
// Elapsed time reads 00:19.54

06:56SuspendingClockマシンがスリープしているときにタイミングを一時停止し、ContinuousClockストップウォッチのように走り続けてください。ジョブが 14 秒間中断された場合、SuspendingClock は 5.4 秒のみカウントし、ContinuousClock は 19.54 秒をカウントします。

キーポイント:

  • アニメーションおよびデバイス関連の時間ロジックに使用されますSuspendingClock
  • 人間の時間測定用ContinuousClock

デバウンス: 検索入力のデバウンス

(08:27) ユーザーが検索ボックスに素早く入力する場合、文字が変更されるたびに検索リクエストがトリガーされることは望ましくありません。

デバウンス アルゴリズムは、値を発行する前にサイレント期間を待機します。

class SearchController {
  let searchResults = AsyncChannel<SearchResult>()

  func search<SearchValues: AsyncSequence>(_ searchValues: SearchValues)
    where SearchValues.Element == String
  {
    let queries = searchValues
      .debounce(for: .milliseconds(300))

    for await query in queries {
      let results = try await performSearch(query)
      await channel.send(results)
    }
  }
}

キーポイント:

  • debounce(for:)デフォルトで使用されるContinuousClock
  • イベントが急速に押し寄せる場合、新しいイベントが発生するたびにタイマーがリセットされます
  • 最後の値は、300 ミリ秒間新しいイベントがない場合にのみ出力されます。

チャンク: メッセージをバッチで送信する

(09:46) ユーザーが素早く入力する場合は、メッセージごとに個別のリクエストを送信するのではなく、バッチを保存してまとめて送信することをお勧めします。

Chunked アルゴリズムは、シーケンスを時間または数量に基づいてチャンクに分割します。

let batches = outboundMessages.chunked(
  by: .repeating(every: .milliseconds(500))
)

let encoder = JSONEncoder()
for await batch in batches {
  let data = try encoder.encode(batch)
  try await postToServer(data)
}

キーポイント:

  • chunked(by:)時間、量、コンテンツによるチャンク化をサポート
  • エラーは再スローされ、コードが安全に保たれます
  • 500 ミリ秒のバッチ。リクエストの数が減るだけでなく、ユーザーが長時間待機することも防ぎます。

AsyncSequence からコレクションに戻る

(10:47) AsyncSequence を通常のコレクションに変換し直す必要がある場合があります。 Swift 非同期アルゴリズムはイニシャライザを提供します。

// Create a message with awaiting attachments to be encoded
init<Attachments: AsyncSequence>(_ attachments: Attachments) async rethrows {
  self.attachments = try await Array(attachments)
}

キーポイント:

  • Array(asyncSequence)Set(asyncSequence)Dictionary(asyncSequence)すべてサポートされています
  • 限られた既知の AsyncSequence でのみ動作します
  • コードを一度にすべて非同期に変更するのではなく、段階的に移行できます。

詳細

Zip の同時実行実装の詳細

ZIP は単純な順次走査ではありません。内部では各入力シーケンスの次の値を同時に待機します。

// Zip の並行処理の特性を示す擬似コード
let videoStream = transcodeVideos()      // AsyncSequence<Video>
let previewStream = generatePreviews()   // AsyncSequence<Preview>

for try await (video, preview) in zip(videoStream, previewStream) {
  // video と preview は同じインデックス位置から来る
  // video が先に準備できた場合は preview を待つ。逆の場合も同じ
  try await upload(video, preview)
}

キーポイント:

  • Zip は内部で構造化された同時実行性を使用して、複数のシーケンスを同時に読み取ります。
  • いずれかのシーケンスが終了すると、Zip も終了します (他のシーケンスの残りの要素を待ちません)
  • エラーの伝播は即座に行われ、現在の操作の他のシーケンスが完了するのを待ちません。

結合と圧縮の選択

// Zip: ペアで使用する必要があり、数は一致している必要がある
for try await (a, b) in zip(streamA, streamB) {
  // a と b は N 番目の要素
}

// Merge: 先に値を生成したものを先に使用し、数は異なっていてもよい
for try await value in merge(streamA, streamB) {
  // value は streamA または streamB から来て、どちらが先に生成したかで決まる
}

(05:08) マージでは、すべてのシーケンスの要素タイプが同じであり、出力が単一要素ストリームである必要があります。 Zip はタプルを生成し、各シーケンスの要素タイプ情報を保持します。

時計の測定方法

let clock = ContinuousClock()
let duration = await clock.measure {
  // 測定する処理
  await fetchData()
  await processData()
}
print("所要時間: \(duration)")

キーポイント:

  • measure開始時刻と終了時刻を自動的に記録する
  • 返されましたDuration印刷したり、直接比較したりできます
  • クロージャ内の例外は上向きに伝播します

重要ポイント

1.コールバック駆動のメッセージング システムを AsyncStream に置き換えます

既存のデリゲート/コールバック メッセージング システムを AsyncStream に移行します。各メッセージ ソースは AsyncSequence になり、コンシューマはfor-await-inトラバース。 Merge は複数ソースのマージに使用され、Zip はペアリング操作に使用されます。

入り口:AsyncStreamAsyncThrowingStream

2.タイマーと GCD 遅延をデバウンス + クロックに置き換えます

すべて使用Timer.scheduledTimerまたはDispatchQueue.asyncAfter実装された手ぶれ補正およびスロットル ロジックは、Swift 非同期アルゴリズムのデバウンス/スロットルに置き換えることができます。コードはより簡潔になり、ライフサイクルは自動的に管理されます。

入り口:searchQuery.debounce(for: .milliseconds(300))

3. Chunk を使用してメッセージのバッチ処理を実装します

チャット アプリ、ログのアップロード、データ分析などのシナリオでは、高頻度の小さなリクエストをバッチ リクエストに変換できます。チャンク化アルゴリズムは、時間枠と量という 2 つの戦略をサポートしており、手動によるバッファーのメンテナンスよりもはるかに簡単です。

入り口:messages.chunked(by: .repeating(every: .seconds(1)))

4.適切な時計を選択してください

プロジェクト内のすべての時間関連ロジックを確認し、「デバイス指向」のシナリオと「人間指向」のシナリオを区別します。 SuspendingClock はアニメーションとタイムアウトの検出に使用されます。 ContinuousClock は、ユーザーの待ち時間と課金ロジックに使用されます。

入り口:SuspendingClock()ContinuousClock()

関連セッション

コメント

GitHub Issues · utterances