非同期プログラミングのサポート#
MoonBit は、Kotlin に似たコルーチンベースの非同期プログラミング方式を採用しています。MoonBit の非同期プログラミングは 2 つの要素から成ります。1 つは async 関数に対するコンパイラサポート、もう 1 つは公式の非同期ランタイム moonbitlang/async です。現在、moonbitlang/async は native バックエンドを最もよくサポートし、JavaScript バックエンドは限定的にサポートしていますが、WebAssembly バックエンドはまだサポートしていません。moonbitlang/async の API は安定版とは見なされておらず、将来変更される可能性があります。
はじめに#
moonbitlang/async を使って非同期プログラミングを行うには、まずプロジェクトで moon add moonbitlang/async@0.19.2 を実行して、moonbitlang/async を依存関係に追加してください。moon.mod.json で "preferred-target": "native" を設定しておくのもよいでしょう。次に、moon.pkg で moonbitlang/async と moonbitlang/async ライブラリ内の他のパッケージを import すれば、非同期プログラミング API を使えるようになります。ワークフロー重視の例が見たい場合は、Native CLI Quickstart を参照してください。
moonbitlang/async に含まれるパッケージ一覧と詳細なドキュメントは mooncakes.io で確認でき、moonbitlang/async の GitHub リポジトリには便利な examples もあります。この記事では、moonbitlang/async の基本的な概念と、特に重要な API をいくつか紹介します。
Async 関数#
Async 関数は async キーワードで宣言します。これらは暗黙に raise し、そうでない場合は noraise を明示的に宣言する必要があります。
async fn my_async_function() -> String {
let (response, body) = @http.get("https://www.moonbitlang.com")
guard response.code is (200..<300) else {
fail("server responded with \{response.code} \{response.reason}")
}
body.text()
}
MoonBit は静的型付け言語なので、コンパイラが async であることを追跡します。そのため、async 関数は通常の関数と同じように呼び出せます。MoonBit IDE では、async 関数の呼び出しが別のスタイルで強調表示されます。上のコードスニペットをMoonBit IDE で開くと、@http.get 関数が下線付きの斜体で表示されるはずです。
Async 関数は async 関数の内部からしか呼び出せません。async 関数を呼び出すと、呼び出し元はブロックされ、呼び出し先が返るまで待機します。これは多くの他の言語における await に似ています。
MoonBit は非同期プログラミングを第一級にサポートしています。async fn main で非同期のプログラムエントリを宣言でき、async test で非同期コードのテストを書けます。非同期テストは、デフォルトで自動的に並列実行されます。async fn main と async test を使うには、パッケージ内で moonbitlang/async を import する必要がある点に注意してください。
構造化並行性とタスクグループ#
非同期プログラムが async 関数を直接呼び出すだけ(つまり await だけを使う)なら、その制御フローは線形で、通常の同期プログラミングと変わりません。非同期プログラムと同期プログラムの根本的な違いは、複数のタスクを生成して並列に実行できることです。この能力は、同時実行タスクによってプログラムの制御フローが大幅に複雑になるため、タスクを堅牢に管理するという新しい課題も生みます。
moonbitlang/async ライブラリは、タスク管理の問題を解決し、非同期プログラムの堅牢性を高めるために 構造化並行性 のパラダイムを採用しています。moonbitlang/async では、新しいタスクを生成できるのは task group の内部のみであり、task group は @async.with_task_group 関数を通じてのみ作成できます:
async fn[Result] with_task_group(
f : async (@async.TaskGroup[Result]) -> Result,
) -> Result
with_task_group 関数は新しい task group を作成し、その task group の中で新しいタスクを生成し、グループ自身を引数として f を新しいタスクの中で実行します。f はその後、spawn_bg などのさまざまな方法を使って、さらに新しいタスクを生成できます:
/// Spawn a new task in the group and let it run in the background
fn[Result] @async.TaskGroup::spawn_bg(
group : TaskGroup[Result],
f : async () -> Unit,
...
) -> Unit
構造化並行性の肝は、with_task_group に対する次の規則にあります:
with_task_groupは、グループ内のすべてのタスクが終了した後でのみ戻ります
with_task_group は、どのような条件でも上記の性質を保証します。通常は、with_task_group はタスクが普通に完了するのを待つだけです。致命的なエラーなどの理由で with_task_group をすぐに終了する必要がある場合(デフォルトでは、子タスクのいずれかが失敗すると with_task_group 自体もすぐに失敗し、エラーが黙って無視されないようにします)、すべての子タスクを適切にキャンセルし、そのクリーンアップ処理が終わるまで待ちます。要するに、with_task_group の規則により、孤児タスク(プログラムがキャンセルし忘れたせいで動き続ける不要なタスク)が moonbitlang/async で存在し得なくなります。
with_task_group を使って複数のタスクを作成し、並列に実行させる簡単な例は次のとおりです:
async test "with_task_group" {
let log = []
@async.with_task_group(group => {
group.spawn_bg(() => {
for _ in 0..<3 {
log.push("task #1 tick")
@async.sleep(200) // sleep for 200ms
}
})
group.spawn_bg(() => {
@async.sleep(100)
for _ in 0..<3 {
log.push("task #2 tick")
@async.sleep(200)
}
})
})
json_inspect(log, content=[
"task #1 tick", "task #2 tick", "task #1 tick", "task #2 tick", "task #1 tick",
"task #2 tick",
])
}
with_task_group は非常に強力な構文です。多くの非同期制御フローを模倣できます。例えば、非同期関数をタイムアウト付きで実行する関数は次のように書けます:
async fn with_timeout(timeout : Int, f : async () -> Unit) -> Unit {
@async.with_task_group(group => {
group.spawn_bg(no_wait=true, () => {
@async.sleep(timeout)
raise Failure::Failure("timeout!")
})
f()
})
}
コード自体はとても単純ですが、with_task_group の意味論により、この単純な関数があらゆる境界ケースで正しく動作することが保証されます:
fがタイムアウト前に正常終了した場合、sleep タスクはno_wait=trueで生成されているので、with_task_groupは sleep タスクを待ちません。その規則を守るために、with_task_groupは sleep タスクをただちにキャンセルします。したがってwith_timeout(.., f)は、fが返った直後に不要な遅延なしで戻ります。fが失敗した場合、そのエラーはwith_task_group全体に伝播します。この場合も sleep タスクは自動的にキャンセルされます。タイムアウト時点で
fがまだ実行中なら、sleep タスクが致命的なタイムアウトエラーを送出し、グループ全体が中断されます。この場合もfは自動的にキャンセルされます。
キャンセルによって非同期プログラムはモジュール化される#
前の節では「キャンセル」という言葉が何度も出てきました。実際、キャンセルは非同期プログラミングで非常に重要な要素です。moonbitlang/async では、with_task_group を含むすべての非同期操作がデフォルトでキャンセル可能です。そのため、これらの基本的な非同期操作を組み合わせてより大きなプログラムを作っても、どれほど複雑であっても自動的にキャンセル可能になります。
非同期コードの一部がキャンセルされると、そのキャンセル信号は、以前ブロックしていた地点で送出されるエラーとして表現されます。そのため、キャンセルを特別に処理する必要はありません。キャンセル信号は自動的にプログラム全体に伝播し、defer やエラーハンドラ内のクリーンアップ処理を発火させます。
任意の async コードをキャンセルできることにより、MoonBit の async プログラムは非常にモジュール化しやすくなります。moonbitlang/async パッケージには、タイムアウト制限や自動リトライなどを行う便利な combinator が多数あり、それらはすべてキャンセル機構に依存して正しく動作します。例えば、次のプログラムは HTTP リクエストをタイムアウト付きで試み、リトライ回数は最大 3 回に制限します:
async fn make_request() -> String {
@async.retry(Immediate, max_retry=3, () => {
@async.with_timeout(1000, () => {
let (response, body) = @http.get("https://www.moonbitlang.com")
guard response.code is (200..<300) else {
fail("the HTTP request is not successful")
}
body.text()
})
})
}
外部世界とのやり取り#
moonbitlang/async は、非同期プログラミングのプリミティブに加えて、非同期 IO 操作用のイベントループも提供します。さらに、http/https、ファイル IO、ソケット IO、プロセス生成など、豊富な IO 操作を 十分な性能 付きで備えています。サポートされる操作の完全な一覧とそのドキュメントは mooncakes.io に、簡単な例は GitHub リポジトリ にあります。以下では、よく使う機能をいくつか手早く見てみましょう:
async fn download_file(url : String, file_name : String) -> Unit {
// perform the transfer lazily to save memory
let (_response, body) = @http.get_stream(url)
defer body.close()
let out_file = @fs.create(file_name, permission=0o644)
defer out_file.close()
out_file.write_reader(body)
}
JavaScript サポート#
moonbitlang/async は native バックエンドを最もよくサポートしますが、JavaScript バックエンドも基本的にサポートしています:
task group や timeout など、IO に依存しない API はすべて利用できます
IO 関連 API は利用できません。なぜなら、すべての JavaScript 実行環境(たとえばブラウザ)がそれらをサポートしているわけではないからです
moonbitlang/async/js_asyncは、外部の JavaScript Promise を待機したり、MoonBit のasync関数を JavaScript Promise として公開したりするなど、JavaScript Promise との統合をサポートします。これにより、JavaScript ホストのネイティブな非同期操作とやり取りできます。
詳細は moonbitlang/async/js_async の mooncakes.io ページ を参照してください。