前回に続いて写経しながら要点だけこの記事に書き起こす形で実践Rustプログラミング入門のPart1のCharpter3~を読んでみる。
前回の記事
https://ikeyu0806.hatenablog.com/entry/2022/03/13/094219
ゼロコスト抽象化
C++やJavaのようなオブジェクト指向プログラミング言語にはカプセル化やポリモーフィズムのような抽象化のテクニックがある。
しかし抽象化のテクニックを使うと抽象化されたコードから具体的な動作を導くための処理を行う必要が出てくることもあり、実行時に余計な負荷がかかってしまう言語も多くある。
ゼロコスト抽象化は抽象化した処理を実行するために必要な負荷を可能なかぎり小さくすることを意味している。
traitとdyn
多くのオブジェクト指向言語ではクラスを使ってカプセル化やポリモーフィズムを実現している。一方、Rustではトレイトを使ってポリモーフィズムを実現している。
トレイトは、様々な型に共通のメソッドを実装するように促すことができる仕組み。
Javaのインターフェイスに似た機能を提供し、必要なメソッドや型を定義することで、トレイトごとの共通の振る舞いをさまざまな構造体や列挙型に適応できる。
trait Tweet { fn tweet(&self); fn tweet_twice(&self) { self.tweet(); self.tweet(); } fn shout(&self) { println!("Uoooooooooooohh!!!") } } struct Dove; struct Duck; impl Tweet for Dove { fn tweet(&self) { println!("Coo!") } } impl Tweet for Duck { fn tweet(&self) { println!("Quack!") } } fn main() { let dove = Dove {}; dove.tweet(); dove.tweet_twice(); dove.shout(); let duck = Duck {}; let bird_vec: Vec<Box<dyn Tweet>> = vec![Box::new(dove), Box::new(duck)]; for bird in bird_vec { bird.tweet(); } }
一般的にメソッドの呼び出し方法には、動的ディスパッチと静的ディスパッチの2つがある。 動的ディスパッチはメソッドが呼び出されたとき、呼び出したインスタンスを確認し、それに合わせた処理を実行時に決める方式。 静的ディスパッチはコンパイル時に実行する処理を解決してしまう方法。
静的ディスパッチでは高速な処理ができるがどちらのインスタンスが呼び出したメソッドなのか、アプリケーションを動かしてみるまで分からない状況もある。
そうした状況ではdynを使って動的ディスパッチができる。
マーカトレイト
メソッドのない、それぞれの持つ意味や役割をしるしのように付与するトレイト。以下一例
- Copy: 値の所有権を渡す代わりに、値のコピーを行うようにする
- Send: スレッド境界を超えて所有権を転送できることを示す
- Sized: メモリ上でサイズが決まっていることを示す
- Sync: スレッド間で安全に参照を共有できることを示す
ジェネリクス
ジェネリクスを使うことでどんな型でも動作できるような処理を作れる
fn make_tuple<T, S>(t: T, s: S) -> (T, S) { (t, s) } fn main() { let t1 = make_tuple(1, 2); let t2 = make_tuple("Hello", "World"); let t3 = make_tuple(vec!([1, 2, 3]), vec![4, 5]); let t4 = make_tuple(3, "years old"); }
所有権と借用
Rustの重要なキーワードとして、所有権、借用、ライフタイムという言葉がある。 Rustではそれぞれの値に所有権があり、その所有権を持っているのは必ず一つの変数だけ。
所有権は元の所有者が持ったままその値を参照する権利をもらうことを借用という。
借用している間に所有者の変数が値を破棄してしまった場合は破棄されたメモリ領域を見ることになってしまう。 そのため参照には安全に利用できる期間を明確にする必要がある。この期間をライフタイムと呼ぶ。
ムーブセマンティクス
多くのプログラミング言語では、ある変数と別の変数を=でつなげれば、値が右から左へとコピーされた。 コピーなので右の変数も同じ値が残っていて左の変数が同じ値を持つことになる。
Rustの場合変数は常に束縛され、その所有権を持つことになる。 ある変数と別の変数を=でつなげれば、右の変数が所有していた値が左の変数の所有になる。
struct Color { r: i32, g: i32, b: i32 } fn main() { let a = Color{r:255, g:255, b:255}; let b = a; // 所有権が譲渡される println!("{} {} {}", b.r, b.g, b.b) }
借用
ある関数に引数で値を渡すとき、値の所有権ごと渡してしまうと呼び出し元の処理に再び所有権を返すのは面倒。
fn main() { let mut important_data = "Hello, World!".to_string(); important_data = calc_data(important_data); println!("{}", important_data); } fn calc_data(data: String) -> String{ println!("{}", data); data }
参照を使って書くとこうなる。値の所有権を譲渡するのではなく値へのアクセスを許す方法。
fn main() { let important_data = "Hello, World!".to_string(); calc_data(&important_data); println!("{}", &important_data) } fn calc_data(data: &String) { println!("{}", data) }
不変な参照はいくつでも参照を渡すことができる。
fn main() { let x = 5; let y = &x; let z = &x; dbg!(x); dbg!(y); dbg!(z); }
一方、可変な参照の場合、一度に渡せる数は1つだけ。
fn main() { let mut x = 5; { let y = &mut x; let z = &mut x; //ここでエラー dbg!(y); dbg!(z); } { let y = &x; let z = &mut x; //ここでエラー dbg!(y); dbg!(z); } }
また、変更の可否に関わらず、参照は元の所有者のライフサイクルよりも長く生存できない。
fn main() { let y; { let x = 5; y = &x; dbg!(x); } dbg(y); // xよりもyが長く生存できずエラー }
RAII
RAIIは「Resorce Acquisition Is Initialization」の略で「リソースの取得は初期化である」という意味。 変数の初期化時にリソースの解放を行い、変数を破棄するときにリソースの解放を行うということ。 これを正しく行わないと、リソースのリークの問題に遭遇することになる。
Rustの場合は変数がスコープから抜けたりすれば即座に確保していたリソースも解放されることになる。
リソースはメモリだけでなく開いているファイルや、ネットワーク接続なども解放される。
RustではデストラクトとしてDropトレイトというものが用意されている。
struct Droppable; impl Drop for Droppable { fn drop(&mut self) { println!("Resource will be released!"); } } fn main() { { let d = Droppable; } println!("The Droppable should be released at the end of block."); }
スレッド安全性
スレッドを扱うには標準ライブラリのthreadを使う。thread::spawnはクロージャを受け取り、それを新しいスレッドで実行する関数。
use std::thread; fn main() { thread::spawn(|| { println!("Hello, world"); }); }
これを実行しても"Hello World"が表示される前にプログラムが終了して何も表示されない場合がある。 必ず表示してから終了するには次のようにする。
use std::thread; fn main() { let handle = thread::spawn(|| { println!("Hello, world!"); }); dbg!(handle.join()); }
thread::spawnからスレッドのハンドルが返ってくるので、そのハンドルのjoinを呼ぶことでスレッドの終了を待つことができる。
spawnに渡すものはクロージャなので変数をスレッドに渡すことができる。
use std::thread; fn main() { let mut handles = Vec::new(); for x in 0..10 { handles.push(thread::spawn(|| { println!("Hello, world!: {}", x); })); } for handle in handles { let _ = handle.join(); } }
この書き方だとクロージャがデフォルトでは変数の参照をキャプチャするためエラーになる。 スレッドの生存期間は変わらないためもしかしたらxの寿命よりも長くなるかもしれない。
このエラーを修正するにはmoveキーワードを使ってxの所有権をスレッドに移す。
use std::thread; fn main() { let mut handles = Vec::new(); for x in 0..10 { handles.push(thread::spawn(move || { println!("Hello, world!: {}", x); })); } for handle in handles { let _ = handle.join(); } }
スレッド間の情報共有
実際のマルチスレッドプログラミングでは各スレッドは実行中も様々な情報を共有しながら協調して作業を進行させる。 マルチスレッドプログラミングにおいてスレッド間で情報を共有する方法としてRustでは次の2つの方法が提供される。
- 共有メモリ
- メッセージパッシング
共有メモリ
共有メモリはその名の通り複数のスレッドで同じメモリ領域を共有する方法。
use std::thread; fn main() { let mut handles = Vec::new(); let mut data = vec![1; 10]; for x in 0..10 { handles.push(thread::spawn(move || { data[x] += 1; })); } for handle in handles { let _ = handle.join(); } dbg!(data); }
このコードはdataの所有権が1つ目のスレッドに移動したため、2つ目のスレッドで所有権を取ることができずエラーになる。 このエラーを解消するには各スレッドで所有権を共有する方法があるが、その目的のためにRcという型が用意されている。
Rcは所有権を共有するために、所有権を保持している所有者の数をカウントしている。所有者が0になったところでメモリを解放するという仕組み。
use std::rc::Rc; use std::thread; fn main() { let mut handles = Vec::new(); let mut data = Rc::new(vec![1; 10]); for x in 0..10 { let data_ref = data.clone(); handles.push(thread::spawn(move || { data_ref[x] += 1; })); } for handle in handles { let _ = handle.join(); } dbg!(data); }
これをコンパイルすると別のエラーになる。 今度は「Rcがスレッド間を安全に渡せない」というメッセージでエラー。 実はRcはマルチスレッドでは正しく動作しない。Rcが持つ参照カウンタは、複数のスレッドが同時にアクセスした場合に壊れてしまう可能性があるため。
そのためRcのマルチスレッド版としてArcというものが用意されている。(ArcはAtomically Referenced Countedの略)
以下単純にRcをArcに置き換えた実装
use std::sync::Arc; use std::thread; fn main() { let mut handles = Vec::new(); let mut data = Arc::new(vec![1; 10]); for x in 0..10 { let data_ref = data.clone(); handles.push(thread::spawn(move || { data_ref[x] += 1; })); } for handle in handles { let _ = handle.join(); } dbg!(data); }
再度コンパイルするとまた別のエラーになる。 Arcは書き換え不能。今回のケースでは各スレッドが別々の要素を参照することがわかっているが一般的には必ずしもそうとは限らない。 そのためArcでは書き換えを行うことはせず、別途排他制御を行う構造が必要になる。そのような用途のために提供されているのがMutex
use std::sync::{Arc, Mutex}; use std::thread; fn main() { let mut handles = Vec::new(); let mut data = Arc::new(Mutex::new(vec![1; 10])); for x in 0..10 { let data_ref = data.clone(); handles.push(thread::spawn(move || { let mut data = data_ref.lock().unwrap(); data[x] += 1; })); } for handle in handles { let _ = handle.join(); } dbg!(data); }
これでエラーなく実行できる。
Mutexはlockというメソッドを提供する。これはMutexに複数のスレッドが同時にアクセスしないことを保証するためのメカニズム。 あるスレッドがdataへの参照を得ると、それ以外のスレッドはlockが完了しなくなる。 dataへの参照を得るのは常に1つのスレッドだけであることを保証する。これを排他制御という。
メッセージパッシング
メッセージパッシングとは各スレッドがメッセージをやり取りしながら動作する方法。 Rustはこのメッセージパッシングに対応したスレッド間通信用のチャンネルを持っている。
use std::sync::mpsc; use std::thread; fn main() { let (tx, rx) = mpsc::channel(); let handle = thread::spawn(move || { let data = rx.recv().unwrap(); println!("{}", data); }); let _ = tx.send("Hello, world"); let _ = handle.join(); }
チャンネルはmpsc::channel関数で作成できる。その戻り値は送信・受信インスタンスのタプルとなっている。 このうち受信側のrxをスレッドに移動して、tx側から"Hello、World"というメッセージを送っている。 メッセージを送る際はsend、受け取る際はrecvを使う。
use std::sync::mpsc; use std::thread; fn main() { let mut handles = Vec::new(); let mut data = vec![1; 10]; let mut snd_channels = Vec::new(); let mut rcv_channels = Vec::new(); for _ in 1..10 { let (snd_tx, snd_rx) = mpsc::channel(); let (rcv_tx, rcv_rx) = mpsc::channel(); snd_channels.push(snd_tx); rcv_channels.push(rcv_rx); handles.push(thread::spawn(move || { let mut data = snd_rx.recv().unwrap(); data += 1; let _ = rcv_tx.send(data); })); } for x in 0..10 { let _ = snd_channels[x].send(data[x]); } for x in 0..10 { data[x] = rcv_channels[x].recv().unwrap(); } for handle in handles { let _ = handle.join(); } dbg!(data); }
これはスレッド間を安全に渡すことのできない型をチャンネルを通して送ろうとしているのでコンパイルエラーになる。
SendとSync
Sendを実装した型はその所有権をスレッドをまたいで送信できることを示す。 Sendはほとんどの型に実装されているが一部の型には実装されていない。先ほどRcを別のスレッドに送信しようとしてエラーになったのはこれが原因。
Syncは複数のスレッドから安全にアクセスできることを示す。例えばMutexはlockメソッドによる排他制御によって複数のスレッドから安全にアクセスできるのでSyncを実装している。
非同期処理
asyncとawaitが用意されており非同期処理ができる。
use futures::executor; struct User { // 何かデータを持っている } struct UserId(u32); struct Db {} impl Db { sync fn find_by_user_id(&self, user_id: UserId) -> Option<User> { } } async fn find_by_user_id(db: Db, user_id: UserId) -> Option<User> { db.find_by_user_id(user_id).await } fn main() { executor::block_on(find_by_user_id(Db {}, UserId(1))) }
Future
RustではFutureを理解しておくことでasyn/awaitの動作に理解が深まる。
Futureとは、ざっくり言うと「将来のどこかで処理が完了するタスク」 他の言語ではPromiseと呼ばれるケースもある。
もう少し具体的な表現で言うと「将来に値が確定する計算のステートマシンを抽象化したインターフェース」。
Futureトレイトは以下のシグネチャを持つ。pollというメソッドだけを持ち、Poll<Self::Output>を返す、という形になっている。
pub trait Future { type Output; fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>; } pub enum Poll<T> { Ready(T), Pending, }
Futureトレイトを実装したタスクが「将来的に値が確定する計算」となる。タスクが作成された時点ではまだ実行されておらず、ランタイムに乗った時点でスケジューリングされ実行される。その実行するかの判断にpoll() すなわちポーリングによってチェックされる。
チェックする主体はWaker(std::task::Waker)であり、poll関数の引数として渡されるContext内にラップされている。Poll::Pendingが返されると返されると、poll()はまた別のタスクが実行状態になるまで呼ばれず、他のタスクを実行する。
Poll::Ready
Futureはゼロコスト抽象化を達成している。各Futureはステートマシンとして表現されており、一箇所のヒープに割り当てされる。 ステートマシンの中にはIOイベントごとにひとつの状態を格納している。
このように、各タスクに対して決まったメモリで一箇所にしか割り当てしないため、余計なメモリ割り当てのコストがかからない。したがってFutureはゼロコスト抽象化を達成できている。
以下Futureの動作を実際に確認できるサンプル。cargo add futuresした後に実行できる。
use futures::{executor, future::join_all}; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; struct CountDown(u32); impl Future for CountDown { type Output = String; fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<String> { if self.0 == 0 { Poll::Ready("Zero!!!".to_string()) } else { println!("{}", self.0); self.0 -= 1; cx.waker().wake_by_ref(); Poll::Pending } } } fn main() { let countdown_future1 = CountDown(10); let countdown_future2 = CountDown(20); let cd_set = join_all(vec![countdown_future1, countdown_future2]); let res = executor::block_on(cd_set); for (i, s) in res.iter().enumerate() { println!("{}: {}", i, s); } }
作成時に呼ばれたu32型の数値を1回poll()が呼ばれるごとにカウントダウンしていき、0になるまではWakerに「また呼んで」と言いつつ(wake_by_ref())Poll::Pendingを返し続け、0であればPoll::Readyとともに”Zero”という文字列を返しています。
main関数内ではこのCountDownタスクを2つ作成し、まとめてexecutor::block_on()メソッドに渡している。executor::block_on()は渡したFutureが完了になるまでブロックして待つメソッド。
async/await
簡単な例
use futures::executor; async fn async_add(left: i32, right: i32) -> i32 { left + right } async fn somethig_great_async_function() -> i32 { let ans = async_add(2, 3).await; println!("{}", ans); ans } fn main() { executor::block_on(somethig_great_async_function()); }
Futureを使って以下のように記述することもできるがasync/awaitを使った方が処理の見通しがよくなるなどのメリットがある。
async fn somethig_great_async_function() -> impl Future<Output = i32> { async { let sns = async_add(2, 3).await; println!("{}", sns); sns } }
複数のawaitが存在した場合、awaitごとに値を受け取る。
async関数はその都度awaitしない限り、中身の評価が走らない。
ムーブ
asyncブロックとクロージャにはmoveキーワードを使用できる。 async moveブロックは、変数がasync moveブロックでも生存できるようにその変数の所有権を移す。
fn move_to_async_block() -> impl Future<Output = ()> { let outside_variable = "this is output".to_string(); async move { // moveキーワードを用いたことにより、変数の所有権をasyncブロックの中に移し、ブロック内でも使用できるようにした println!("{}", outside_variable); } }
ライフタイム
スレッドを跨いでFutureを送りたくなった際、`staticライフタイムを用いる必要がある。
下記では所有権のない変数を引数に渡そうとしたためコンパイルエラーになる
fn some_great_function() -> impl Future<Output = i32> { let value: i32 = 5; send_to_another_thread_with_borrowing(&value) // コンパイルエラー } async fn send_to_another_thread_with_borrowing(x: &i32) -> i32 { // 何か別スレッドへ送る処理が書かれている想定 }
asyncブロック内にvalueを書き、その中でさらにsend_to_another_thread_with_borrowing関数を呼び出すようにしてみる。 valueの所有権を引き伸ばすことで対処する。
fn some_great_function() -> impl Future<Output = i32> { async { let value: i32 = 5; send_to_another_thread_with_borrowing(&value).await } }
非同期ランタイム
非同期ランタイムは、非同期計算の実行環境を指す。非同期ランタイムには現在いくつか種類があるが代表的なクレートは下記の2つがある。
- tokio
- async-std
tokio
Rustをずっと支えてきた非同期ライブラリで多くのRustライブラリが現在もtokioに依存した実装。
使うにはCargo.tomlに以下追加してcargo build実行。 macrosフィーチャを追加して#[tokio::main]というアトリビュートを利用可能にしている。 これによりmain関数をasync化できる。
tokio = { version = "0.2.21", features = ["macros"] }
以下のようにプログラムを書ける。
async fn add(left: i32, right: i32) -> i32 { left + right } #[tokio::main] async fn main () { let ans = add(2, 3).await; println!("{}", ans); }
async-std
近年登場してきたクレート。tokioは内部実装やAPIが少々複雑という問題点があり、もう少し直感的でわかりやすい非同期ランタイムを作ろうというモチベーションのもと、発足した。
使うにはCargo.tomlに以下追加してcargo build実行。
async-std = { verion = "1.6.1", features = ["attributes"] }
以下のようにプログラムを書ける。
async fn add(left: i32, right: i32) -> i32 { left + right } #[async_std::main] async fn main () { let ans = add(2, 3).await; println!("{}", ans); }
async-trait
Rustでは現状トレイトの関数にasyncをつけることができない。つまり下記のようなコードはコンパイルエラーになる。
trait AsyncTrait { async fn f() { println!("Couldn't compile"); } }
使うには以下実行
cargo add async-trait
トレイトに#[async_trait]というアトリビュートをつけると、async fn..という関数宣言をできるようになる。
use async_trait::async_trait; #[async_trait] trait AsyncTrait { async fn f() { println!("Could compile"); } }
もしトレイトの中身は実装先に委ねたい場合は実装先(impl)にも#[async_trait]アトリビュートを追加することで、async fnという関数宣言を同様にできるようになる。
#[async_trait] trait AsyncTrait { async fn f(&self); } struct Runner {} #[async_trait] impl AsyncTrait for Runner { async fn f(&self) { println!("Hello, async-trait"); } }