Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rust速習会3

Avatar for Masaki Hara Masaki Hara
November 01, 2018

 Rust速習会3

Avatar for Masaki Hara

Masaki Hara

November 01, 2018
Tweet

More Decks by Masaki Hara

Other Decks in Programming

Transcript

  1. Rocket rocket.rs • 不安定版コンパイラで動作 / 非同期は未対応 • 構文拡張 (独自の #[])

    をふんだんに使って書きやすくしてい る • そのため、nightlyの更新ですぐに壊れる • 全体的に作りこまれていて人気っぽい
  2. Tide (wg-net) • まだ存在しないフレームワーク • RustのNetwork Services Working Groupで計画されているモ ジュラーなフレームワーク

    • これ自体が流行るかは別として、成果物は各フレームワークに も還元されると思われる • コードだけでなくドキュメントも成果物の一部として規定され ているので、そちらも要注目
  3. そもそも非同期I/Oとは? • 元々の動機: I/Oは待ち時間が発生するので、できる処理から先 に進める仕組みにしたい 新しい接続リクエストが来たので、これを接続13とする。次のリクエストを受け付けられる状態にしておく。 接続13の最初の35バイトがあるのでパースする。データ不足でパースできなかったのでバッファに溜める。 新しい接続リクエストが来たので、これを接続14とする。次のリクエストを受け付けられる状態にしておく。 接続14の最初の45バイトがあるのでパースする。リクエストの内容がわかったのでデータベースに問い合わせにいく。データベースからはすぐには返 答はこない。

    その間に接続13の続きの35バイトが来たのでパースする。まだ十分な情報がないのでバッファに溜める。 また新しい接続リクエストが来たので、これを接続15とする。キューが多すぎるので追加のリクエストはOSに溜めてもらう。 接続13の続きの6バイトが来て、リクエストの内容がわかったので、ファイルを探しに行く。ファイルが見つかったのでヘッダーと最初の20バイトを送 信する。 その間に接続14のためにデータベースにしていた問い合わせが返ってきたので、これをもとにもう一度データベースに問い合わせる。 接続13に追加の35バイトを送信する。 特に何もできないので待機する。 接続13に追加の30バイトを送信する。 接続14のためにデータベースにしていた問い合わせが返ってきたので、レスポンスを作ってヘッダーと最初の20バイトを送信する。 接続13から13バイトが来た。閉じてよいというリクエストなので閉じる。キューが開いたので次のリクエストを受け付けられる状態にしておく。 さっそく新しい接続リクエストが来たので、これを接続16とする。キューが多すぎるので追加のリクエストはOSに溜めてもらう。 ………
  4. そもそも非同期I/Oとは? • 元々の動機: I/Oは待ち時間が発生するので、できる処理から先 に進める仕組みにしたい • ……しかし、実際のプログラムはできるだけ直列的に書きたい。 接続13については パースできるまで読む。76バイト 読み込んでリクエストがわかった。

    ファイルを探しにいく。 ファイルが見つかったので合計85 バイト送る。 パースできるまで読む。13バイト 読んでcloseリクエストだったので 閉じる。 接続14については パースできるまで読む。45バイト 読み込んでリクエストがわかった。 データベースに問い合わせる。 その結果をもとにまたデータベー スに問い合わせる。 レスポンスを作って送信する。 …… 接続15については ……
  5. 並行実行を誰が司るか? • 選択肢2: OSのスレッド(またはプロセス)管理に任せる • CPUの並列性をいい感じに使ってくれる • そこそこスケールするけど10000並列とかは難しい CPU #0

    今は接続13を処理している CPU #1 今は接続14を処理している スレッド 50733 接続13を処理している スレッド 51252 接続14を処理している スレッド 52000 接続15を処理している OS 定期的にどれかのCPUで起動して、CPUを割り当て直す
  6. 並行実行を誰が司るか? • 選択肢3: プログラミング言語/ライブラリレベルの抽象化 • スレッド並列性をさらにラップしている • OSスレッドを使うよりもスケールする(らしい) CPU #0

    CPU #1 スレッド 50733 スレッド 51252 スレッド 52000 エクゼキュータ 軽量スレッド1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 エクゼキュータ エクゼキュータ
  7. 並行実行を誰が司るか? • 選択肢3: プログラミング言語/ライブラリレベルの抽象化 • スレッド並列性をさらにラップしている • OSスレッドを使うよりもスケールする(らしい) CPU #0

    CPU #1 スレッド 50733 スレッド 51252 スレッド 52000 エクゼキュータ 軽量スレッド1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 エクゼキュータ エクゼキュータ 高性能といわれるサーバーは、この方法を採用していることが多い
  8. 非同期I/O戦国時代になった経緯 • 古いRust (~0.11) はグリーンスレッドとlibuvサポートを持っ ていた! use std::task::spawn; // proc

    is the closure which will be spawned. spawn(proc() { println!("I'm a new task") }); ちなみにこの頃のRustは組み込みGCサポートやランタイムリフレクションもあったが、それぞれ 0.12と1.0.0-alphaで削除されている
  9. mioを説明する前に: I/O多重化とは • 例: 接続27と接続28を並行に処理していて、どちらも読み込み 待ち中とする • OSスレッドを使う場合 スレッド 24887

    接続27を処理している スレッド 24890 接続28を処理している OS 接続27から読み込んでください それまでスレッドを止めてください 接続28から読み込んでください それまでスレッドを止めてください
  10. mioを説明する前に: I/O多重化とは • 例: 接続27と接続28を並行に処理していて、どちらも読み込み 待ち中とする • 軽量スレッドを使う場合 スレッド 24887

    接続27と接続28を処理している OS 接続27または接続28のうち、早いほうから読み込んでください それまでスレッドを止めてください
  11. mioを説明する前に: epoll(7) の場合 1. 通知を受けたいイベントを全てファイル記述子(整数)として 表す • dup(2), signalfd(2), eventfd(2)

    などを使う 2. OS側に、イベントの集合を送信する • epoll_create(2), epoll_ctl(2) を使って、逐次的に組み立てる 3. epoll_wait(2)を呼ぶと、指定したイベントのどれかが来るま で待つ。 “(2)” とか “(7)” というのは伝統的なマニュアルの章番号である。Linuxの場合、(1)はコマンド、 (2)はシステムコール、(3)はCライブラリ関数、(7)はまとまった解説になっている。
  12. mio – I/O多重化の薄い抽象レイヤ • mio (Metal I/O): epollやkqueueなどのI/O多重化システムコー ルを薄く抽象化するライブラリ •

    使い方はepollとほぼ一緒なので、人類が直接使うのには向いて いない 人類が使うための抽象化を別途行う必要がある
  13. おまけ: ノンブロッキングI/O • I/O多重化に加えて、以下の操作が必要 • これは O_NONBLOCK などとして比較的標準化されているので、mio のような専用の抽象化は要らない スレッド

    24887 接続27と接続28を処理している OS 接続27から読んでください。 すぐに読めないときはブロックせずに戻ってください。
  14. おまけ: 真の非同期I/O • OSから能動的な通知を受け取る仕組みもある (aio(7) などを参 照) スレッド 24887 接続27と接続28を処理している

    OS 接続27から読んでください。 今はすぐ復帰して、あとで完了したら通知してください。
  15. Future トレイト • 「時間がかかりそうだったら別のことをやる」をするためのア プローチの一つ • 親が根気強く poll を呼び続けるとやがて答えが返ってくる 親「もしもし、今計算できそうですか

    (poll を呼ぶ)」 Future「まだですよ (NotReady)」 親「もしもし、今計算できそうですか (poll を呼ぶ)」 Future「まだですよ (NotReady)」 親「もしもし、今計算できそうですか (poll を呼ぶ)」 Future「できました! (Ready(42))」 Future「もう今後は呼ばないでね」 Futureには先物(未来の価格での取引を保証する金融派生商品)という意味があり、そちらが由来では ないかとも言われている
  16. Future トレイト • ざっくり定義すると、こんな感じ pub trait Future { type Output;

    /// 完了したら `Some`, 途中だったら `None` を返す。 /// `Some` が一度でも返ったら、それ以上呼んではいけない。 fn poll(&mut self) -> Option<Self::Output>; } 実はIteratorと同じ。しかし、IteratorはNoneが出るまで呼び続けるのに対して、FutureはSomeが 出るまで呼び続ける。
  17. Futureを作ってみよう • 計算途中の状態を覚えておく構造体 pub struct Fib { n: u32, //

    残りループ回数 a: u32, // 今のフィボナッチ数列の項 b: u32, // 次のフィボナッチ数列の項 }
  18. Futureを作ってみよう • Fibが最終的に整数を返せるようにしていく impl Future for Fib { type Item

    = u32; type Error = (); fn poll(&mut self) -> Result<Async<Self::Item>, Self::Error> { } }
  19. コンビネーターたち(1) future::ok(x) 値を返すだけ future::err(e) エラーを返すだけ future::lazy(|| f()) 非同期関数をあとで実行する futures-0.1, futures-0.2の

    Future は Result と統合されているので、コンビネーターもエラーが考慮さ れている。ここでは「成功」と「失敗」をあわせて「完了」と呼んでいる。 • 基本ブロック
  20. コンビネーターたち(2) fut1.map(|x| f(x)) 成功したら次を実行する (fは普通の関数) fut1.and_then(|x| f(x)) 成功したら次を実行する (fは非同期関数) fut1.map_err(|x|

    f(x)) 失敗したら次を実行する (fは普通の関数) fut1.or_else(|x| f(x)) 失敗したら次を実行する (fは非同期関数) fut1.from_err(|x| f(x)) 失敗を From で変換する fut1.then(|x| f(x)) 完了したら次を実行する (fは非同期関数) • 続けて何かをする系 futures-0.1, futures-0.2の Future は Result と統合されているので、コンビネーターもエラーが考慮さ れている。ここでは「成功」と「失敗」をあわせて「完了」と呼んでいる。
  21. コンビネーターたち(3) fut1.select(fut2) 両方を実行して、どちらか先に完了したほうを使う。 fut1.select2(fut2) 両方を実行して、どちらか先に完了したほうを使う。 違う型を返す Future にも使える future::select_all(iter_fut) 全てを実行して、一番最初に完了したものを使う。

    future::select_ok(iter_fut) 全てを実行して、一番最初に成功したものを使う。 • select系 futures-0.1, futures-0.2の Future は Result と統合されているので、コンビネーターもエラーが考慮さ れている。ここでは「成功」と「失敗」をあわせて「完了」と呼んでいる。
  22. コンビネーターたち(4) fut1.join(fut2) 両方を実行して、両方の完了を待つ fut1.join3(fut2, fut3) 上に同じ (3個) fut1.join4(fut2, fut3, fut4)

    上に同じ (4個) fut1.join5(fut2, fut3, fut4, fut5) 上に同じ (5個) future::join_all(iter_fut) 上に同じ (任意個) • join系 futures-0.1, futures-0.2の Future は Result と統合されているので、コンビネーターもエラーが考慮さ れている。ここでは「成功」と「失敗」をあわせて「完了」と呼んでいる。
  23. コンビネーターたち(5) fut1.join(fut2) 両方を実行して、両方の完了を待つ fut1.join3(fut2, fut3) 上に同じ (3個) fut1.join4(fut2, fut3, fut4)

    上に同じ (4個) fut1.join5(fut2, fut3, fut4, fut5) 上に同じ (5個) future::join_all(iter_fut) 上に同じ (任意個) • ループ系 futures-0.1, futures-0.2の Future は Result と統合されているので、コンビネーターもエラーが考慮さ れている。ここでは「成功」と「失敗」をあわせて「完了」と呼んでいる。
  24. タスク • Future = 非同期Rust世界の関数 • Task = 非同期Rust世界のスレッド •

    各タスクは1つのトップレベルFutureを持つ Task1 AndThen Select read read write タスクの 実行状態
  25. Executor Executor • 複数のワーカースレッドを使う実装もある Worker #1 実行待ちキュー Task6 Task3 Task8

    キューに偏りがある ときは融通しあう (work stealing) Worker #2 実行待ちキュー Task9 Task4 Task1
  26. 通知 • 休眠する(NotReadyを返す)前に、通知を受けられるよう準備し ておく必要がある let task = futures::task::current(); some_watcher.register(task); return

    Ok(Async::NotReady); 現在実行中のタスクのハンドルを取得 ハンドルを誰かに託す 通知を受けるまで休眠する
  27. 通知 • 休眠する(NotReadyを返す)前に、通知を受けられるよう準備し ておく必要がある let task = futures::task::current(); some_watcher.register(task); return

    Ok(Async::NotReady); このタスクが再開できる条件を別スレッドが監視している。 再開できそうになったら通知が行われる task.notify(); イベントの種類ごとに通知の責務を受けもつ媒体をドライバーという
  28. 通知が間違っているとどうなるか • 偽陰性の場合 (再開可能なのに通知されない) • 眠りっぱなしになる • 大変困る • 偽陽性の場合

    (意味もなく通知される) • 無駄pollが1回増えるだけ • 困らない • selectで遅いほうのイベントが完了したとき(後の祭り)とかに起こる
  29. 間違ってブロックするとどうなるか • OSスレッドはCPUのタイマー割り込みで強制的にスレッドを 切り替える仕組みがある (preemptive) が、Rustのタスクには この機能はない (non-preemptive, cooperative) •

    タスクが間違ってブロックすると、進むはずのタスクまで巻き 添えを食らう。 • スレッドプールベースの場合、何個か同時にブロックしてはじめて発 覚するのでたちが悪い • ブロッキングI/O、Mutex、重い計算などでタスクをブロック しないように気を遣う必要がある
  30. tokio tokioランタイム tokio-threadpool executor worker worker worker task task task

    task task task task task task 各種ドライバー tokio-reactor mio を使ってネットワークを 駆動する tokio-fs ファイルシステム tokio-timer タイマー register notify
  31. tokio-core • Tokioプロジェクトの古いランタイムライブラリ • tokio-rfcsにデザインドキュメントがある • tokio で書かれたライブラリは tokio-core ランタイムと互

    換性がある • tokio-core で書かれたライブラリは tokio ランタイムと互 換性がない • ライブラリが先に新しくなる必要がある!
  32. Actix • Rustで書かれたアクターフレームワーク • Tokioと多少の互換性がある • Tokio-reactor を使っているが、tokio-threadpool は使ってい ない

    (独自executorで動かしている) • したがって、tokio-threadpool の存在を仮定しているライブラ リは動かない可能性がある
  33. futures-0.2 yank事件 • 諸事情あってfutures-0.2はyankされて別の名前で再公開された (futures-preview 0.2) • yankされると、既存の Cargo.lock を使わない限り解決されなくなる

    • 特にちゃんとしたアナウンスもなくyankされた • この「yank事件」は結構議論になった • gtk-rs とかが futures-0.2 を使っているらしい
  34. futures-awaitの重大な制約 • 借用がawaitをまたぐことができない #[async] fn foo() { let conn =

    await!(connect()); let result = await!(conn.get(bar)); conn.close(); } こういうのが使えない (普通にFuturesを使っても書けない)
  35. futures-awaitの重大な制約 • 借用がawaitをまたぐことができない #[async] fn foo() { let conn =

    await!(connect()); let (conn, result) = await!(conn.get(bar)); conn.close(); } 所有権を渡して、あとで同じものを返してもらう というインターフェースになる
  36. 借用とFuture • Rustの構造体として書こうとするとこんな感じ struct FooState { conn: Connection, conn_ref: &'self.conn

    mut Connection, } こういう気持ちは表明できない こういうのを自己参照構造体 (self-referencial struct) あるいは貫入データ構造 (intrusive data structure) という
  37. ?Move の顛末 • Sizedと同様に、デフォルトで有効のトレイト Move を導入す る提案があったが、いくつかの懸念から却下された • これ以上デフォルトトレイトを追加すると混乱が増える •

    Moveできない型は、関数から返すこともできないから生成することも できない。この部分を解決するにはさらに多くの道具が必要になる
  38. Pinned reference (RFC 2349) • ムーブを禁止するのではなく、後からムーブできるような参照 の取り方を禁止してしまえばよいというアイデア • どんなデータも、最初はムーブできる。 •

    しかし、 Pin<&mut T> という特殊な参照を取ると、その中身 はムーブできない(ムーブしないままDropする必要がある) • これは Pin<Box<T>> というラッパーや pin_mut!() というマクロ によって担保される • ただし、貫入データ構造でない場合は、 Pin<&mut T> から &mut T を復元する余地が残されている。
  39. 真のasync/await (RFC 2394) • Pin API により、参照を自然に使える • 処理系に組み込みなので、マクロ特有の面倒くささがない async

    fn foo() { let conn = await!(connect()); let result = await!(conn.get(bar)); conn.close(); } async/awaitは鋭意開発中 futures-0.3 と nightly を組み合わせることで試せる
  40. futures-0.3 pub trait Future { type Output; fn poll(self: Pin<&mut

    Self>, lw: &LocalWaker) -> Poll<Self::Output>; } std::task に移動 Pinned referenceを受け取るようになった。 Resultとの統合が廃止された。 ReadyとNotReadyの2択になった。 Futures-0.2 に導入されていたcontextが 単純化された。 通知用のハンドルだけを持ち回す。
  41. Dieselの準備 • MySQLとPostgreSQLのdevライブラリを入れておく。例: $ sudo apt install libpq-dev libmysqlclient-dev $

    brew install postgresql mysql dieselがデフォルトで両方を要求するので、両方入れておく。 必要なほうを入れておいて、あとでdieselのオプションを指定するのでもOK
  42. データベースの接続確認 $ psql postgres:///todoapp psql (10.5 (Ubuntu 10.5-0ubuntu0.18.04)) Type "help"

    for help. todoapp=# ¥q 以下のような亜種が有効かも: postgres://localhost/todoapp postgres://username:@localhost/todoapp postgres://postgres:@localhost/todoapp postgres://username:password@localhost/todoapp postgres://postgres:password@localhost/todoapp
  43. プロジェクトを立てる • Cargo.tomlに依存関係を追加 [dependencies] log = "0.4.6" env_logger = "0.5.13"

    dotenv = "0.13.0" serde = "1.0.80" serde_derive = "1.0.80" serde_json = "1.0.32" actix-web = "0.7.13"
  44. プロジェクトを立てる • Cargo.tomlに依存関係を追加 [dependencies] log = "0.4.6" env_logger = "0.5.13"

    dotenv = "0.13.0" serde = "1.0.80" serde_derive = "1.0.80" serde_json = "1.0.32" actix-web = "0.7.13" 依存関係を変えたらとりあえず しておくと便利 $ cargo build
  45. プロジェクトを立てる • extern crateしておく #[macro_use] extern crate log; extern crate

    env_logger; extern crate dotenv; extern crate serde; #[macro_use] extern crate serde_derive; extern crate serde_json; extern crate actix_web; log, serde_derive由来のマクロを使うので明示 最新安定版の1.30.0からは通常のuseで マクロをインポートすることができる。 また1.31.0からはデフォルトでRust 2018が 有効になるため、extern crateは不要になる。
  46. サーバーを起動する use actix_web::{server, App}; fn main() { env_logger::init(); debug!("Launching an

    app..."); server::new(|| App::new()).bind("[::]:8080").unwrap().run(); } イントラネットに公開したくない場合は "localhost:8080"
  47. サーバーを起動する • これでサーバーは立ち上がる $ cargo run DEBUG 2018-11-01T06:44:05Z: todo_app: Launching

    an app... INFO 2018-11-01T06:44:05Z: actix_net::server::server: Starting 2 workers INFO 2018-11-01T06:44:05Z: actix_net::server::server: Starting server on [::]:8080 ブラウザからアクセスすると404が返ってくるはず
  48. エンドポイントを追加する use actix_web::{http, Path, Responder}; fn ping(_: Path<()>) -> impl

    Responder { "pong" } 既存の行にいい感じに追加してください
  49. サーバーを再起動する $ RUST_LOG=todo_app=debug,actix=info cargo run DEBUG 2018-11-01T06:44:05Z: todo_app: Launching an

    app... INFO 2018-11-01T06:44:05Z: actix_net::server::server: Starting 2 workers INFO 2018-11-01T06:44:05Z: actix_net::server::server: Starting server on [::]:8080 /pingにアクセスするとpongが返ってくる
  50. Askamaを使う • Cargo.tomlに依存関係を追加 [dependencies] askama = { version = "0.7.2",

    features = ["with-actix-web"] } [build-dependencies] askama = "0.7.2" 両方必要
  51. Askamaを使う • build.rs というファイルをプロジェクト直下に作成する extern crate askama; fn main() {

    askama::rerun_if_templates_changed(); } build.rs はコンパイル時に実行される。 そのため以下の特徴がある • クロスコンパイル時もホストアーキテクチャにコンパイルされる • 依存関係が区別される。 ([build-dependencies])
  52. テンプレートを書く • templates/base.html <!doctype html> <title>{% block title %} default

    title {% endblock %}</title> {% block content %} <p>default content</p> {% endblock %}
  53. テンプレートを書く • templates/index.html {% extends "base.html" %} {% block title

    %}Top{% endblock %} {% block content %} <h1>Top</h1> <p>Hello, Askama!</p> {% endblock %}
  54. テンプレートを使う #[macro_use] extern crate askama; use askama::Template; #[derive(Debug, Template)] #[template(path

    = "index.html")] struct IndexTemplate; テンプレートの描画に必要な引数は、この構造体に詰める 最新安定版の1.30.0からはuse askama::Templateで Templateのderive macroもインポートされるので、 #[macro_use] は不要になる
  55. テンプレートを使う fn index(_: Path<()>) -> impl Responder { IndexTemplate }

    … App::new() .route("/", http::Method::GET, index) .route("/ping", http::Method::GET, ping) 今のところテンプレート引数はないので、こうなる ここまで書いて実行すると描画される
  56. 静的ファイルをサーブする use actix_web::fs::StaticFiles; … App::new() .handler("/static", StaticFiles::new("./static").unwrap()) .route("/", http::Method::GET, index)

    .route("/ping", http::Method::GET, ping) ./static を / からサーブしたりするのはもう少し工夫が必要そう。 複数個の StaticFiles を作ると無駄スレッドが大量にできるので、 こちらも工夫が必要
  57. データベース • Cargo.tomlに依存関係を追加 [dependencies] chrono = "0.4.6" diesel = {

    version = "1.3.3", features = ["postgres"] } cargo build を流しつつ次に進もう
  58. データベース • up.sql ができているので以下のようにする CREATE TABLE todos ( id BIGSERIAL

    PRIMARY KEY, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL, content VARCHAR NOT NULL )