レッスン4 — Rust: ライフタイム・Box・Arc・dyn Trait
問い
ExEx ソースの最初の 10 行で Arc<>、'static、dyn Trait、Box<> が立て続けに登場。Rust の「上級だが実は単純」な 4 機能 — Reth ソースを読むには全部必要、ExEx シグネチャを記憶から書けるか?
原理(最小モデル)
'a= この借用は何のスコープと同じくらい生きるか. コンパイラへの注釈、多くは推論で省略可能。'static= プログラム終了まで生きる.&'static strリテラル、ExEx のような「いつ終わるか分からないバックグラウンドタスク」に頻出。Box<T>= ヒープに置く. 再帰データ構造 / 動的サイズ値(dyn Trait)/ ムーブで安価。Rc<T>vsArc<T>. Rc = シングルスレッド参照カウント / Arc = マルチスレッド対応(Atomic 参照カウント)。Reth/ExEx はArc<...>だらけ。Mutex<T>/RwLock<T>で共有 + 書き換え. Arc 単独 = 読み取り専用、書き換えるなら Mutex(読み書き排他)or RwLock(読み並列、書き排他)。.lock().unwrap()は poisoning で panic。dyn Trait= 動的ディスパッチ. 実行時メソッド解決、Java/TS interface 風。コストは vtable 経由の呼び出し。impl Traitvsdyn Trait. impl = 静的(コンパイル時)、dyn = 動的(実行時 + Box 必要)。ヘテロコレクションは dyn。- ExEx シグネチャの語彙.
async fn my_exex<Node: FullNodeComponents>(mut ctx: ExExContext<Node>) -> eyre::Result<()>。
具体例
ライフタイム 'a:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() { x } else { y }
}
'static の例:
let s: &'static str = "hello";
tokio::spawn に渡すクロージャが 'static を必要とする理由: spawn は future を独立タスクとして駆動、いつ終わるか分からない → クロージャがキャプチャするデータも「いつまでも生き続ける」必要 → 'static 制約。
Box<T> でヒープ:
let boxed: Box<i64> = Box::new(42);
println!("{}", *boxed); // 42
3 用途: 再帰データ構造(enum List { Cons(i32, Box<List>), Nil } — Box なしだとサイズ無限大でコンパイル不可)+ 動的サイズ値(Box<dyn Trait>)+ 大きい値の安価ムーブ。
Rc vs Arc:
| 型 | 用途 |
|---|---|
Rc<T> | シングルスレッドで参照カウント共有 |
Arc<T> | マルチスレッド対応(Atomic 参照カウント) |
use std::sync::Arc;
let shared = Arc::new(String::from("hello"));
let clone1 = Arc::clone(&shared); // 参照カウント+1
let clone2 = Arc::clone(&shared); // 参照カウント+2
// 別スレッドへ送れる(Arc は Send)
std::thread::spawn(move || println!("{}", clone1));
Arc::clone は内部 T をディープコピーしない → 参照カウントを atomic に +1 するだけ(low コスト)。"A" = Atomic(Compare-And-Swap で thread-safe)。
Mutex で書き換え:
use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0));
let c = Arc::clone(&counter);
std::thread::spawn(move || {
let mut n = c.lock().unwrap();
*n += 1;
});
| 型 | 用途 |
|---|---|
Mutex | 読み書き両方を排他 |
RwLock | 読みは並列、書きは排他 |
.lock().unwrap() の panic = poisoning(あるスレッドが lock 中に panic で死ぬ → 後続の lock が panic 連鎖)。Reth のコードに .lock().unwrap() 至る所、クラッシュタイミングを理解。
dyn Trait:
trait Greet {
fn greet(&self);
}
struct En;
struct Ja;
impl Greet for En { fn greet(&self) { println!("Hello"); } }
impl Greet for Ja { fn greet(&self) { println!("こんにちは"); } }
let g: Box<dyn Greet> = if std::env::var("LANG").unwrap_or_default().starts_with("ja") {
Box::new(Ja)
} else {
Box::new(En)
};
g.greet();
impl Trait vs dyn Trait:
| 構文 | 意味 |
|---|---|
impl Trait | 静的ディスパッチ(コンパイル時に型確定) |
dyn Trait | 動的ディスパッチ(実行時に解決、Box が必要) |
Box<dyn Greet> のコスト = vtable 経由(実行時に「実装の関数ポインタテーブル」を引いてメソッド呼び出し)。Box<En> は静的に展開、vtable 不要。性能は impl 勝つが、ヘテロコレクション(Vec<Box<dyn Trait>>)が必要なら dyn。
ExEx シグネチャ:
async fn my_exex<Node: FullNodeComponents>(
mut ctx: ExExContext<Node>,
) -> eyre::Result<()> {
while let Some(notification) = ctx.notifications.recv().await {
// ...
}
Ok(())
}
注釈:
Node: FullNodeComponents= トレイト境界ExExContext<Node>= ノードバンドルにジェネリック- 内部で
Arc<...>でコンポーネント共有 - ライフタイム省略だが
'static要求
失敗例(誤解)
「Rc を別スレッドに送れる」— 間違い。Rc は !Send(コンパイラが拒否)、参照カウントが atomic でないので競合データレース可能性。マルチスレッド共有は Arc 必須。
「.lock().unwrap() は OK」— 危険。poisoning(lock 保持中のスレッドが panic)で連鎖 panic。本番では .lock().expect("...") でメッセージ + 致命的なら適切なリカバリ。
「impl Trait が常に dyn より速い」— 半分間違い。impl は静的ディスパッチで vtable なし → ホットループで速い。ただし dyn は コードサイズ が小さい(モノモーフ化爆発回避)+ ヘテロコレクション可能。用途次第。
ステップで組み立てる
Step 1: ライフタイム 2 種を即答
'a = スコープ注釈、'static = プログラム終了まで(バックグラウンドタスク頻出)。
Step 2: Box<T> の 3 用途
再帰データ + 動的サイズ + 安価ムーブ。
Step 3: Rc vs Arc
シングル vs マルチスレッド、Atomic 参照カウント。Reth/ExEx は Arc。
Step 4: Mutex / RwLock の使い分け
Mutex = 読み書き排他、RwLock = 読み並列 + 書き排他。poisoning に注意。
Step 5: dyn Trait と vtable
実行時メソッド解決、Box 必要、ヘテロコレクション可能、vtable 経由のコスト。
Step 6: ExEx シグネチャを記憶
async fn my_exex<Node: FullNodeComponents>(mut ctx: ExExContext<Node>) -> eyre::Result<()>。
答え合わせ
'staticがバックグラウンドタスクに頻出する理由:tokio::spawnの future はいつ終わるか不明 → クロージャがキャプチャするデータも「いつまでも生き続ける」必要 →'static制約。ExEx のような「ノード寿命と同じくらい生きる」タスクで頻出。Arc::cloneの正確なコスト: 参照カウンタを atomic に +1 するだけ(コンペア & スワップ 1 回)= ナノ秒オーダー。T のディープコピーは起きない → 共有が安価。"A" = Atomic(thread-safe な参照カウント増減)。dyn Traitの vtable コスト: 実行時に「実装の関数ポインタテーブル」を引く間接呼び出し(branch prediction が効きにくい + キャッシュミス可能性)= ホットループで impl Trait より遅い。ただし ヘテロコレクション必須(Vec<Box<dyn Stage>>など)の場合は dyn 一択。
合格基準
'aと'staticの差を即答できる。Box<T>の 3 用途を言える。- Rc / Arc の使い分けと Atomic の意味を言える。
- Mutex / RwLock の使い分けと poisoning を言える。
- impl Trait と dyn Trait の使い分けを言える。
- ExEx シグネチャを記憶から書ける。
まとめ(3行)
- ライフタイム(
'a/'static)+Box(ヒープ)+ Arc(マルチスレッド共有)+ Mutex/RwLock(書き換え)+ dyn Trait(動的ディスパッチ)= Reth ソース読みの 5 語彙。 - Arc::clone は atomic +1(ナノ秒、ディープコピーなし)、Mutex.lock の panic は poisoning で連鎖、dyn は vtable コストとヘテロコレクション可能のトレードオフ。
- ExEx シグネチャ
async fn my_exex<Node: FullNodeComponents>(mut ctx: ExExContext<Node>) -> eyre::Result<()>を記憶から書けるのが習得証拠。