Introduction
本記事はSE-0317 async let bindings(68dd955時点)を紹介します。正確には、SE-0317内でサラッと流されてしまっている用語や説明を深掘りします。なので本筋の説明はありません。
このプロポーザルの主役はタイトル通り async let です:
async let veggies = chopVegetables()
async let meat = marinateMeat()
async let oven = preheatOven(temperature: 350)
このプロポーザルの前提にはSE-0304 Structured concurrency、つまり Task があります。 async と書いてあるのでSE-0296 Async/awaitの続きに見えますが Task の話です。いずれにせよこれらのプロポーザルはSwift 5.5から = Swift Concurrencyデビュー当時から導入されています。
勝手に読み心地を評価するとこんな感じ:
| 項目 | 星 | コメント |
|---|---|---|
| 難易度 | ★★☆☆☆ | Structured Concurrencyと Task の知識が求められます。 |
| 長さ | ★★★☆☆ | 770行のマークダウン。プロポーザル界では普通です。 |
| 重要度 | ★★☆☆☆ |
async let を酷使するなら読んでおいてよさそう。 |
予めSE-0304を読んでおくとよいでしょう。
Motivation
この記事はSwift Advent Calendar 2025用に書かれました。
個人的なモチベはこちら:
いろいろ深堀りする!
fan-outパターン、scatter/gatherパターン
Task groups are a very powerful, yet low-level, building block useful for creating powerful parallel computing patterns, such as collecting the "first few" successful results, and other typical fan-out or scatter/gather patterns.
タスクグループは非常に強力だが低レベルな構成要素であり、"最初のいくつか"の成功した結果を集めるなどの強力な並列計算パターンや、その他の典型的なファンアウトやスキャッター/ギャザーのパターンを作るのに役立つ。
fan-outのfanは扇、扇状です。fan-outパターンについてはこちらの記事などが詳しいです。Goの記事はいくつかヒットしますがSwiftやiOSでは馴染みが薄そう。
scatter/gatherパターンの説明の例:
fan-outパターンのほうも検索してみるとAWSの構成の話が出てきたりします。
global, width-limited, concurrent executor
By default, child tasks use the global, width-limited, concurrent executor, in the same manner as task group child-tasks do.
デフォルトでは、子タスクはタスクグループの子タスクと同様に、幅が制限されたグローバルな並行エグゼキュータを用いる。
width-limitedという形容詞が挟まっていますがきっと普通のglobal concurrent executorのことでしょう。
グローバル変数 globalConcurrentExecutor の説明にもサイズが固定であると書かれています。ちなみにこのグローバル変数はSwift 6.0(iOS 18.0)からなのでSwift Concurrencyデビュー当時は使えない代物です。
By default it uses a fixed size pool of threads and should not be used for blocking operations which do not guarantee forward progress as doing so may prevent other tasks from being executed and render the system unresponsive.
デフォルトでは固定サイズのスレッドプールを使用しており、forward progressを保証しないブロッキング操作に使用すべきではない。そうしてしまうと他のタスクの実行を妨げてシステムが応答い状態になる可能性がある。
https://developer.apple.com/documentation/swift/globalconcurrentexecutor
async let は今も昔もglobal concurrent executorを使っているようですが、nonisolated async funcの挙動はSE-0338 Clarify the Execution of Non-Actor-Isolated Async Functions(Swift 5.7)で変わっています。
具体的には、Swift 5.6以下ではnonisolated async funcを呼び出す際に普通に await するか一旦 async let するかで挙動が変わる可能性がありました。今は両者ともglobal concurrent executorが実行するので挙動は同じになります。
func doSomething() async -> Bool {
Thread.isMainThread
}
@MainActor
func doSomethingOnMainActor() async {
await print(doSomething()) // Swift 5.6ではtrue, 5.7+ではfalse
async let result = doSomething()
await print(result) // false
}
トップレベルでの async let の利用
It is not allowed to declare
async letas top-level code, in synchronous functions or closures:
async letをトップレベルコードや同期関数・同期クロージャ内で宣言することは許されない:
Swift 5.5の時点ではトップレベルにasync/awaitを書くこと自体ができませんでした。
しかしその後のSE-0343 Concurrency in Top-level Code(Swift 5.7)の時に解禁されています。SE-0343のDetailed designのサンプルコードに async let も登場します。
func theAnswer() async -> Int { 42 }
async let a = theAnswer() // implicit await, top-level is async
await theAnswer() // explicit await, top-level is async
closed-over variable
For example, it will result in a compile-time error, preventing a potential race condition, for a
async letinitializer to attempt mutating a closed-over variable:
例えば、async letイニシャライザがキャプチャした変数を変更しようとすると、潜在的な競合状態を防ぐためコンパイル時エラーになる:
ここで登場するclosed-overは、この後に続くサンプルコードを見たところではcapturedと同じ意味と考えてよさそうです。
Rustでも同じ意味っぽい気がする。
only for closures that do not capture (close over) any local variables can be casted to function pointers.
脚注ではありますがcapture (close over)と書いてあります。
Pythonはどうでしょう。
ところがJavaScriptだとちょっと違うみたい……?
Here, the module exports a pair of getter-setter functions, which close over the module-scoped variable
x.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures
ここでは、モジュールはモジュールスコープの変数xを隠蔽するゲッターセッター関数のペアをエクスポートします。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Closures
キャプチャしていること自体は変わらないようですが、キャプチャを利用して情報隠蔽する文化があるっぽい?JavaScript詳しい人いたら教えてください。
Sugaring rule
To understand the execution semantics of the above snippet, we can remember the sugaring rule that the right-hand side of a async let effectively is just a concurrently executing asynchronous closure:
上記スニペットの実行セマンティクスを理解するには、async letの右辺は実質的に並行実行される非同期クロージャにすぎないというsugaring ruleを思い出せばよい:
sugarはれっきとした動詞です。ちなみにdesugarという動詞もSwift Evolutionでは時々登場し、日本語に訳すと脱糖です(Wikipedia情報)。
sugaring ruleにはサンプルコードがついています、がエラーが出ます。
async let (l, r) = { // Function produces expected type '(String, String)'; did you mean to call it with '()'?
return await (left(), right())
}
ちゃんと () をつけて呼んであげないとダメです。SE-0317は全体的にそういうところがあります。
関係性を書き表すと多分こんな感じ:
async let (l, r) = { return await (left(), right()) }()
// ↓ ↑
// sugar ↓ ↑ desugar
// ↓ ↑
async let (l, r) = (left(), right())
パターンとスロー
async let (yay, nay) = ("yay", throw Boom())
try await yay // because the (yay, nay) initializer is throwing
というサンプルコードが載っていますがこれはSwift 5.5だろうがSwift 6.2だろうが通らない気がする。
async let (yay, nay) = ("yay", throw Boom()) // Cannot convert value of type 'String' to specified type '(_, _)'
async let (yay, nay): (String, Boom) = ("yay", throw Boom()) // Cannot convert value of type 'String' to specified type '(String, Boom)'
エラーを投げるのをやめたら通ります、このサンプルコードの意味はなくなりますが……。
クロージャ
async let とキャプチャ
It is legal to capture a
async letin a non-escaping asynchronous closure, like this:
次のように、非エスケープの非同期クロージャでasync letをキャプチャすることは合法である:
と書いてありますが、escapingかどうかに関わらずエラーになります。
func greet(_ f: () async -> String) async -> String { await f() }
async let name = "Alice" // Capturing 'async let' variables is not supported
await greet { await name }
この後の「Cancellation and async let child tasks」のサンプルコードも assert(_:_:file:line:) に渡すところで同様のエラーが出ます。
このエラー文の出元は多分このへん。
- https://github.com/swiftlang/swift/blob/956dc0c1a258891e4248f0f2b9aba081376db7e8/lib/Sema/TypeCheckCaptures.cpp#L233-L239
- https://github.com/swiftlang/swift/blob/956dc0c1a258891e4248f0f2b9aba081376db7e8/include/swift/AST/DiagnosticsSema.def#L4948-L4950
if (auto var = dyn_cast<VarDecl>(VD)) {
// `async let` variables cannot currently be captured.
if (var->isAsyncLet()) {
Context.Diags.diagnose(capture.getLoc(), diag::capture_async_let_not_supported);
return;
}
}
// Cannot capture `async let`
ERROR(capture_async_let_not_supported,none,
"capturing 'async let' variables is not supported", ())
is not supportedというエラー文、そして上記のcurrentlyというコメントを見る限りでは、プロポーザルで思い描いた async let になれていなさそうです。
ちなみに報告はされている模様。
async let とキャプチャとヒープとスタック
It is not legal to escape a
async letvalue to an escaping closure. This is because structures backing the async let implementation may be allocated on the stack rather than the heap. This makes them very efficient, and makes great use of the structured guarantees they have to adhere to. These optimizations, however, make it unsafe to pass them to any escaping contexts:
async letの値をエスケープクロージャへエスケープすることは合法ではない。これは、async letの実装を裏付ける構造体がヒープではなくスタックに割り当てられる場合があるためである。これにより非常に効率的になり、従うべき構造化された保証を大いに活用できる。しかし、これらの最適化により、エスケープするコンテキストにそれらを渡すことは安全ではなくなる:
ヒープとスタックについてはこの辺に書いてあります。
Pitchには次のように書いてあります。
Allowing handles to a child task to escape the scope of their parent task would break the guarantee we want to make about child tasks, that their lifetimes are always bounded by a scope,
Agreed 100x, this is true about a lot of computation and enables a lot of optimizations, e.g. the stack allocation optimization we already do for non-escaping closures.
子タスクのハンドルが親タスクのスコープを逃れることを許せば、子タスクの寿命は常にスコープで制限されるべきという保証を壊してしまう。
まったくの同意で、これは多くの計算に当てはまり、多くの最適化を可能にする。例えば非エスケープクロージャに対して既に行っているスタック割り当て最適化がある。
SE-0317: Async Let - Evolution / Proposal Reviews - Swift Forums
lifetimeの辻褄と最適化のメリットを取ったからエスケープがNG、と読める気がする?
ハンドルを渡す
async let との比較のために次のサンプルコードが載っています。
func take(h: Task<String, Error>) async -> String {
return await h.get()
}
get() に try がついてないのでこのコードはコンパイルが通りません。SE-0317はそういうところがある。
それはさておいて、この get() はSwift Concurrencyデビュー当初からDeprecatedになっているメソッドです。開発当初は get() でしたが value に取って代わられました。
| 日付 | できごと |
|---|---|
| 2020年9月16日 | Swift 5.3のリリース。 |
| 2020年10月28日 |
mainブランチに get() のスタブが入る。以降実装が進む。 |
| 2021年4月18日 | SE-0304 (2nd review)スレで、Chris Lattner氏が「effectful properties導入を考慮すると get() より value がいい(意訳)」と発言。 |
| 2021年4月26日 | Swift 5.4のリリース。 |
| 2021年5月19日 | mainブランチで get() から value に置換され、 get() はDeprecatedになる。 |
| 2021年5月28日 | release/5.5ブランチでも同様の処置。 |
| 2021年6月3日 | SE-0317のActive Review期間終了。 |
| 2021年9月20日 | Swift 5.5のリリース。 |
| 2021年11月1日 | mainブランチでSwift 5.1にバックポートされる。 |
なのでSwift 5.5やその後のバックポートで我々利用者側にお披露目された時にはすでにDeprecatedなのでした。
まとめ
SE-0317では、サンプルコードの文法が実際と異なったり最新バージョンだとエラーが出るようになっていたり逆に出なくなっていたりします。SE-0317を読む予定のある方には、手元で実際に動かしながら挙動を確認することをお勧めします。