Rust の 非同期処理についての覚え書き - 並列・並行・非同期
対象
内容
前の記事で libc::epoll でのタイマーを作って、非同期処理のひとつをやってみたのですが、今回は非同期処理の概要についての覚え書き。
非同期処理が混乱しやすいのは、「同時に」処理してくれるんじゃないの?という思い込み...。Asynchronous Programming in Rust のこの辺にそういう事が書いてある(と思う)。
- 並列処理(Parallelism)
- 本当に同時に処理するやつ
- 並行処理(Concurrency)
- ちょっとずつやって切り替えて、同時にやってるように見せるやつ
- 非同期処理(Asynchronous)
- ある処理の完了を待たずに次に進むやつ。ある処理ってのは別のやつに押し付ける
非同期処理は、並行処理と似ているけども、自分でやらないってのが違いなんでしょうね。視点が違うというか。
今回注目しているのは非同期処理で、これの肝心なところは、他に押し付けているだけで自分のコードは同時に進められないこと。自分のコードの中で同時に何かやろうと思ったらスレッドとかが必要になってくる。自分のコードの流れはひとつ。
押し付ける先は言語のランタイムだったり、別のスレッドだっ押し付けたり、OS の機能だったりすると思いますが、Rust の場合はコンパイルされて生の形で動作するので、ランタイムが請け負ってくれるわけでなく、OS とか他のどこかで動いているスレッドに押し付ける。
で、シングルスレッドでは眼の前のコードの流れはひとつなので、他に押し付けた仕事の完了を受けるにはどうしても最後は何処かで止まって待ってなきゃいけない。前の記事で epoll_wait() がありましたけど、あんな感じで。
OS にこれやってとお願いしておき、完了したら指定していた残りのコードを実行して戻ってくる。このお願い=登録とか、出来たよって時の復帰の仕方とか、複数の非同期処理をこなすとなると結構面倒で、この辺りの蛇口を揃えてくれているのが Future/async/await と tokio などのランタイムらしい。
ちょっと自分で作ってみようとすると分かりますが、登録はしたはいいけど、メインループに復帰する前にそれまで使っていた変数保存しなきゃいけないとか、処理が終わった連絡が来て結果を受け取った時に他の非同期処理のことをどうするのかとか、やはり自分でやると結構面倒くさい。
これって、中身は OS がタイムシェアリング方式でプロセスのスイッチしているのと同じなんでしょう。OS は自分のコードを細切れにしてやっているから並行処理なんだろうけど。それまで使っていた変数(レジスタ)を退避させたり、スケジュールにタスクを放り込んだり。視点の違いはあれど、並行処理と非同期処理ってのは、対象が自分のコードかどうかだけが違いなんじゃないかと。実際、非同期処理のランタイムはそんなことをお世話してると思う。
- スケジューリングやメインループ、 epoll などの待ち操作を作っているのがランタイム
- 非同期処理させたいコードを、スケジュールに登録できる形にするのが async
- 非同期処理させたいコードの中で、中断させてランタイムのメインループに戻ってこさせるのが await
- async でまとめられたコードが Futureで、中のタスクが終わったかどうかを判断している
- wake() はスケジュールへの登録を呼び起こす
Rust の非同期処理はこんな感じじゃないかと。