ハイライト
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:56)SuspendingClockマシンがスリープしているときにタイミングを一時停止し、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 はペアリング操作に使用されます。
入り口:AsyncStream、AsyncThrowingStream
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()
関連セッション
- Swift の新機能 — Clock API の導入背景を含む、Swift 5.7 の新機能の概要
- Swift で分散アクターを紹介 — 分散アクターと AsyncSequence を組み合わせてリアルタイム システムを構築する
- Swift でのプロトコル インターフェイスの設計 — Swift 5.7 ジェネリックスの改善、AsyncSequence の利点
some/any文法
コメント
GitHub Issues · utterances