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

Rust速習会3

Masaki Hara
November 01, 2018

 Rust速習会3

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 )