🙅‍♂️

Rust入門者は非同期Rustをやらないでください

に公開
16

はじめに

すいません、連日の記事があったため少し煽りっぽいタイトルにしてしまいました。
平常時の感覚だったら、「Rustに入門するなら、同期Rustから始めるのがおすすめ」 とかにしてた記事だと思います。

この記事で主張したいことは以下です。

  • Rustは難しい
  • Rustに入門するときの題材に、"Web Backend"や"Wasmでブラウザで動く何か"などの非同期Rustで無ければ達成できない題材を採用している人はそれがRust初級者への道を阻んでいる可能性がある
  • 過去に非同期Rustで入門した人を否定する記事ではない
  • Rustに入門することが目的なら、非非同期Rustから始めた方が良いのではないか?

ここで長く言葉の定義をしても微妙なので、下に言葉の定義 というセクションを用意してあります。

前提

タイトルに書けていない前提を書いています。

この記事の指す「Rust入門者」はLL言語(Python、Ruby、TypeScript、など...)をメインに書いていた人間が入門した前提で書いています。
もともと、C言語やC++などを触っていた人間はあんまり想定にしていません。
また、「理論より実践派的な思考」の人を想定しているので、何か作る前にRust本を読み込んで自分の中で腹落ちさせられるタイプの人間も想定していないです。

なぜ、非同期Rustをやってはダメなのか?

一言でいうと難しいからです。
その難しいという部分をブレイクダウンしていきます。

そもそも、Rustは難しい

Rust入門者にはRustは難しいです。

Rust入門が終わった人間は生存バイアスで「Rustは簡単」とか「ライフタイムさえわかれば余裕」みたいなことを言いますが、嘘だと思って良いです。

C言語やC++などを、学校の授業などでは無く触っていた経験やCSのベーシックな知識がしっかりしていない人間に取ってはRustは難しいと思っています。

Rust初級者になると、心地よく感じてくるコンパイルエラーもLL言語から来た人間は「lint errorをレベルの小言のコンパイルエラー取らないとテスト実行すら出来ないクソ言語」と思ってもしょうがないと思っています。

非同期Rustは高度な抽象化しているから難しい

非同期Rustは一見すると

async fn some_function() {
    let some_value = some_future_function().await;
    let some_value = other_future_function(some_value).await;

    some_value
}

のような形で、非同期プログラミングを手続き方的な書き方で書けて簡単に書ける様に見えます。

しかし、その実裏では以下のようなコードに変換されます。

変換後(長いので)

以下のコードはo1 proに書かせた変換です。
実際のRustコンパイラが生成するコードとは厳密には異なります。非同期関数が内部的にどのような構造に変換されるかを概念的に理解するために貼り付けています

use std::pin::Pin;
use std::future::Future;
use std::task::{Context, Poll};

async fn some_function() -> i32 {
    // 例として i32 を返す想定にしています
    let some_value = some_future_function().await;
    let some_value = other_future_function(some_value).await;
    some_value
}

// ↑上のコードはコンパイラによって↓のような仕組みを持つコードに変換される

// コンパイラが作る匿名の Future 型(ここでは __SomeFunction という名前を仮定)
struct __SomeFunction {
    // 状態管理のためのenum
    state: __SomeFunctionState,

    // 途中で待機するFutureを保持するフィールド(Pinで保持)
    future_1: Option<Pin<Box<dyn Future<Output = i32>>>>,  // some_future_function()の戻り値
    future_2: Option<Pin<Box<dyn Future<Output = i32>>>>,  // other_future_function(...)の戻り値

    // 必要に応じて値を一時的に保持するフィールド
    intermediate_value: Option<i32>,
}

// 状態を表すenum
enum __SomeFunctionState {
    // まだ何も呼んでいない状態
    Start,
    // first future (some_future_function) の完了待ち状態
    WaitingOnFuture1,
    // second future (other_future_function) の完了待ち状態
    WaitingOnFuture2,
    // 完了
    Done,
}

impl Future for __SomeFunction {
    type Output = i32; // 戻り値の型

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        // 安全にフィールドを扱うために一旦参照を得る
        let this = &mut *self;

        loop {
            match this.state {
                __SomeFunctionState::Start => {
                    // some_future_function() を生成して保存
                    let fut = Box::pin(some_future_function());
                    this.future_1 = Some(fut);
                    // 次の状態へ遷移
                    this.state = __SomeFunctionState::WaitingOnFuture1;
                }

                __SomeFunctionState::WaitingOnFuture1 => {
                    // first future がセットされているはずなので取り出す
                    if let Some(fut) = &mut this.future_1 {
                        // future を poll して結果を確認
                        match fut.as_mut().poll(cx) {
                            Poll::Ready(val) => {
                                // 結果を intermediate_value に保存
                                this.intermediate_value = Some(val);
                                // second future を生成して保存
                                let fut2 = Box::pin(other_future_function(val));
                                this.future_2 = Some(fut2);

                                // 状態を次に進める
                                this.state = __SomeFunctionState::WaitingOnFuture2;

                                // 再びループの先頭へ行き、次の状態で処理を継続
                                continue;
                            }
                            Poll::Pending => {
                                // まだ完了していないので、後ほど再度 poll される
                                return Poll::Pending;
                            }
                        }
                    } else {
                        // 本来なら起こり得ないエラー状態
                        panic!("Future for some_future_function not found");
                    }
                }

                __SomeFunctionState::WaitingOnFuture2 => {
                    // second future がセットされているはず
                    if let Some(fut2) = &mut this.future_2 {
                        match fut2.as_mut().poll(cx) {
                            Poll::Ready(val) => {
                                // 完了したらその値を返す
                                // ここでは intermediate_value はもう不要なので使用しない
                                this.state = __SomeFunctionState::Done;
                                return Poll::Ready(val);
                            }
                            Poll::Pending => {
                                return Poll::Pending;
                            }
                        }
                    } else {
                        // 本来なら起こり得ないエラー状態
                        panic!("Future for other_future_function not found");
                    }
                }

                __SomeFunctionState::Done => {
                    // すでに完了しているので、二重に poll されたらエラーなど
                    panic!("Future polled after completion");
                }
            }
        }
    }
}

// 実際の呼び出し時は、some_function() は上記の __SomeFunction 構造体を生成し、
// impl Future<Output = i32> を返す関数として扱われます。
fn some_function_desugared() -> impl Future<Output = i32> {
    __SomeFunction {
        state: __SomeFunctionState::Start,
        future_1: None,
        future_2: None,
        intermediate_value: None,
    }
}

そして、この関数の返り値はsome_functionを実行する為の状態機械を表した型になっておりFuture traitを実装しています。(訳わからない人は読み飛ばしてください)

この抽象化は便利なのですが、Rustの知識が無いとすぐに牙を向いてきます。

例えば以下のようなコードです。

use std::rc::Rc;
use std::cell::RefCell;

async fn process_data() {
    // Rc<RefCell<T>>はSend + Syncを実装していない
    let data = Rc::new(RefCell::new(vec![1, 2, 3]));
    
    // 最初の非同期処理
    let value = fetch_value().await;
    
    // dataはSend + Syncではないため、awaitの境界をまたいで使用できない
    data.borrow_mut().push(value);
    
    // 別の非同期処理
    let result = process_value(data.borrow().clone()).await;
    
    println!("結果: {:?}", result);
}

async fn fetch_value() -> i32 {
    // 何らかの非同期処理
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    42
}

async fn process_value(data: Vec<i32>) -> Vec<i32> {
    // 何らかの非同期処理
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    data.into_iter().map(|x| x * 2).collect()
}

#[tokio::main]
async fn main() {
    tokio::spawn(process_data()).await.unwrap();
}

これはコンパイル通りません[3]

コンパイルエラー
Exited with status 101
Standard Error
   Compiling playground v0.0.1 (/playground)
error: future cannot be sent between threads safely
   --> src/main.rs:34:18
    |
34  |     tokio::spawn(process_data()).await.unwrap();
    |                  ^^^^^^^^^^^^^^ future returned by `process_data` is not `Send`
    |
    = help: within `impl Future<Output = ()>`, the trait `Send` is not implemented for `Rc<RefCell<Vec<i32>>>`
note: future is not `Send` as this value is used across an await
   --> src/main.rs:9:31
    |
6   |     let data = Rc::new(RefCell::new(vec![1, 2, 3]));
    |         ---- has type `Rc<RefCell<Vec<i32>>>` which is not `Send`
...
9   |     let value = fetch_value().await;
    |                               ^^^^^ await occurs here, with `data` maybe used later
note: required by a bound in `tokio::spawn`
   --> /playground/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/task/spawn.rs:168:21
    |
166 |     pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
    |            ----- required by a bound in this function
167 |     where
168 |         F: Future + Send + 'static,
    |                     ^^^^ required by this bound in `spawn`

error: future cannot be sent between threads safely
   --> src/main.rs:34:18
    |
34  |     tokio::spawn(process_data()).await.unwrap();
    |                  ^^^^^^^^^^^^^^ future returned by `process_data` is not `Send`
    |
    = help: within `impl Future<Output = ()>`, the trait `Send` is not implemented for `NonNull<Vec<i32>>`
note: future is not `Send` as this value is used across an await
   --> src/main.rs:15:55
    |
15  |     let result = process_value(data.borrow().clone()).await;
    |                                -------------          ^^^^^ await occurs here, with `data.borrow()` maybe used later
    |                                |
    |                                has type `std::cell::Ref<'_, Vec<i32>>` which is not `Send`
note: required by a bound in `tokio::spawn`
   --> /playground/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/task/spawn.rs:168:21
    |
166 |     pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
    |            ----- required by a bound in this function
167 |     where
168 |         F: Future + Send + 'static,
    |                     ^^^^ required by this bound in `spawn`

error: future cannot be sent between threads safely
   --> src/main.rs:34:18
    |
34  |     tokio::spawn(process_data()).await.unwrap();
    |                  ^^^^^^^^^^^^^^ future returned by `process_data` is not `Send`
    |
    = help: the trait `Sync` is not implemented for `Cell<isize>`
    = note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` or `std::sync::atomic::AtomicIsize` instead
note: future is not `Send` as this value is used across an await
   --> src/main.rs:15:55
    |
15  |     let result = process_value(data.borrow().clone()).await;
    |                                -------------          ^^^^^ await occurs here, with `data.borrow()` maybe used later
    |                                |
    |                                has type `std::cell::Ref<'_, Vec<i32>>` which is not `Send`
note: required by a bound in `tokio::spawn`
   --> /playground/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.44.2/src/task/spawn.rs:168:21
    |
166 |     pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
    |            ----- required by a bound in this function
167 |     where
168 |         F: Future + Send + 'static,
    |                     ^^^^ required by this bound in `spawn`

error: could not compile `playground` (bin "playground") due to 3 previous errors

なぜかわかりますか?

それは、async fn process_data の返り値に Send + Syncが無いからです。
なぜ、async fn process_dataの返り値に Send + Syncが無いのか?
それは、dataSend + Sync が無いからです。

以下の様にするとコンパイルは通るようになります。

/// 返り値がSend + Syncを持っている async fnの実装
async fn process_data() {
    // Arc<Mutex<T>>はSend + Syncを実装している
    let data = Arc::new(Mutex::new(vec![1, 2, 3]));
    
    // 最初の非同期処理
    let value = fetch_value().await;
    
    // dataはSend + Syncなので、awaitの境界をまたいで安全に使用できる
    {
        let mut data_lock = data.lock().unwrap();
        data_lock.push(value);
    }
    
    // 別の非同期処理
    let data_clone = Arc::clone(&data);
    let result = process_value(data_clone).await;
    
    println!("結果: {:?}", result);
}

#[tokio::main]
async fn main() {
    tokio::spawn(process_data()).await.unwrap();
}

この、 問題に対してアコーディオンやコメントで説明しています。

もちろん、この問題に関してもちゃんとRust本を読んでいれば論理的に対処できますが。
入門者に求めることではないかと思いますし、この差分だけを見せられると「アドホックな対処法が多い覚えゲーの言語」[4]という印象を持っても仕方ないかと思います。

このような落とし穴がこれだけでは無くて、無数にあります。

コメント追記分ですがあんまりメンテが追い付いて無くてこの中は議論がぐちゃってるかも

ここで言いたい問題の核は、なぜ tokio::spawnで呼ばれているprocess_dataの返り値が前者では Send + Sync を持たず 後者では持っているのか?
というところになります。

理由は、async関数の返り値(の状態機械)は { awaitを跨いで Send + Syncを持たない値を共有⇒ Send + Sync を持たない} からだと理解しています。(かなり、アバウトな理解をしている自覚はありますが、細かいところで間違っているかもしれません)

例えば、RC<RefCell<T>>を内部で持っていても await を跨いでいなければtokio::spawnから呼び出せてコンパイルが通ります

async fn process_data() {
    // Rc<RefCell<T>>はSend + Syncを実装していないが境界を跨いでいない。
    let mut result = {
        let data = Rc::new(RefCell::new(vec![1, 2, 3]));

        // 最初の同期処理
        let value = fetch_value();

        // dataはSend + Syncではないため、awaitの境界をまたいで使用できない
        data.borrow_mut().push(value);

        // 別の同期処理
        process_value(data.borrow().clone())
    };
    // 別の同期処理
    let other = some_other_async().await;

    result.push(other);

    println!("結果: {:?}", result);
}

async fn some_other_async() -> i32 {
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    return 100;
}

fn fetch_value() -> i32 {
    // 何らかの同期処理
    42
}

fn process_value(data: Vec<i32>) -> Vec<i32> {
    // 何らかの同期処理
    data.into_iter().map(|x| x * 2).collect()
}

つまり、このエラーは「単にtokio::spawnのtrait境界からの要請」のみの問題では無く、非同期関数がどのように状態機械に変換されて返り値を返すか?、その変換のされ方によって返り値のtrait 境界が変わる問題を指摘しています。

LL言語と同じ土俵で勝負してしまうことになるから

Rustは「Web Backendサーバをすぐに作ること」に向いている言語では無いと思っています。
というよりは任意のXに対して
「Xをすぐに作ること」に向いている言語では無いと思っています。

それなのに、Web Backend(非同期Rustが主戦場)からRustに入門してしまうと

「自分の得意なXXXという言語ならこんなに書けるのになんだこのクソ言語は!?」

みたいな感想になりやすいのかと思います。
不気味の谷に近いようにも感じます。
なので、もともと遠い物なので思い切っていつもと違うことで初めて見たらいいのでは無いかなと思っています。

非同期タスクの解決に絶対に非同期Rustが必要では無い

非同期的なタスクを処理することと、非同期Rustを使うことは必ずしもイコールにはなりません。

例えば、CLI tool非同期的な処理が必要になることがありますが、それは必ずしもasync/await構文や非同期ランタイムを使わなければならないということではありません。Rustには他にも非同期処理を行う方法があります。

非同期IOが絡む部分でも、ブロッキングI/Oとスレッドの組み合わせを用いると非同期Rustを必要とせずに問題解決することが出来ます。

これらのアプローチは非同期Rustほど複雑な抽象化を必要とせず、特に入門者にとっては理解しやすいものです。
小〜中規模のCLIツールや単純なデータ処理プログラムでは、こういった方法で十分なパフォーマンスを得られることも多いです。[5] [6]

その他

金言集 に他の方がくれたツイートを載っけてあります。

じゃあどうすれば良いの?

私の主張は「非非同期Rust」(以下NA Rust)から始めて、Rustに慣れてから非同期 Rustに移行しましょうという物になります。

NA Rustの題材としては以下のような物がおすすめです。

CLIツールを作る

https://www.oreilly.co.jp/books/9784814400584/
CLIを題材とした良書があるのでこれを読みながら、NA RustでCLIツールを作るのはかなり良いと思います。
もちろん、CLIでも非同期的なタスクを処理すること(≠ 非同期Rustを使う)はあるのですが、余りマルチタスクにならないので積極的にstd::threadなどを利用して対処すると良いと思っています。
非同期Rustへのウォーミングアップにもなります。

言語処理系を作る

非同期プログラミングが必要なくて、それなりの規模感のコードを書ける題材として言語処理系があります。[7]
特にC言語のコンパイラを作るこの資料が入門としては神資料だと思います。
https://www.sigbus.info/compilerbook

この資料では、C言語を題材としていますがそこを翻訳するところを含めてRustの勉強にもなると思うのでおすすめです。

LL言語で書いてて、CPU boundで遅くなっている処理を部分的に書き換える

これはお仕事編みたいな感じになってしまうかもしれませんが、目的が明確になるのでおすすめです。

CPU boundは絶対条件

大事なことは、必ず・絶対に・間違いなく「CPU bound」になっていることを確認してから取り組むことです。[8]
なんとなく遅いなぁみたいなものを書き換えるのはやめた方が良いです。
少なくとも、その言語が用意している速度プロファイル手法を適用してCPU boundになっていることを確認してから取り組みましょう。

LLじゃない言語からの書き直しはやめましょう

CPU boundの高速化に関して、LL言語からの書き換えはJITが爆裂に効きまくるケースや実は内部で低レベルな言語で記述されている場合を除いては、単純な書き換えで早くなることが多いです。

ただ、C,C++はもちろんのことGo、JVM系、C#、Swift等からの書き直しはやめましょう。
パフォーマンスチューニングに並々ならぬ感覚を持っていない限りはRust入門者の状態では、気持ち早くなりましたぐらいにしかならない可能性があります。

おすすめの取り組み方

LL言語から、Rustで書いたコードを呼び出す方法は以下のようなものがあります。

  1. 元言語用のbindingライブラリを使う(pythonだとpyo3とか)
  2. Rustでその処理を行う、コマンドを作って元言語からコマンド経由で呼び出す
  3. FFI

1, 2は両天秤で比べて良いと思いますが、Rust入門者には3はおすすめしません。自分がRust初級者になったと自負するまでは1, 2の方法で頑張りましょう。
3を選ぶとボス敵(unsafe)に会います。[9]
1, 2の天秤は、

  • 1はRustの知識を要求される可能性が高いがメンテナンス性が高くしやすい
  • 2はRustの知識を要求されないが、メンテナンス性が低くなり安い

という、感じなので自分が投資できる時間などに依って選択は変わると思います。

それでも、非同期Rustで始めたい場合

私は正直に言うと、非同期Rustがめちゃくちゃ得意では無いのでこのセクションはより他人任せになります。

座学の時間を取ろう

さすがに、Rust本を全部読んでた方が良いと思っています。
Rust上の概念をちゃんと理解しておくことが、非同期Rustを始める上で強い助けになります。

あと、tokioチュートリアル(日本版)も読んでおくと良いと思います

強い人間を頼ろう

非同期Rustに強くて積極的に発信している有識者を頼りましょう。(私が勝手に取り上げているだけです)

取り上げた有識者方様へ

こんな記事で、勝手に取り上げてすいません…
嫌だDMでも良いです。で連絡するか、さっきマシュマロ開いたので聞いてくれても良いです。ったらすぐ消すのでDMまでお願いします。

https://x.com/yusuktan

この方は日本語発信者の中で 知識・積極的な活動・誠実な回答 のバランスが良いと思っています。
tokioチュートリアル(日本版)の翻訳者でもあります。

https://marshmallow-qa.com/hagfx13mpgd4f1l

もオープンされているので是非質問してみてください。

KOBA789さん

https://x.com/KOBA789

この方はvtuberもされていて、配信アーカイブも面白いものが多いので見てみると良いと思います。

https://x.com/KOBA789/status/1911150837856100773

このような発信を行っているので、筆者個人としては楽しみにしています。

https://www.youtube.com/@KOBA789

マシュマロとかを開いているわけでは無いので、youtubeの配信の時に質問すると良いと思っています。

そこそこ知ってる人に聞く

https://x.com/higumachan725

はい、私です。他の人にばかり任せていても良くないので書いておきます。

同期Rustはそこそこ書ける自信はありますが、非同期Rustはまあ知ってるかなってぐらいです。
こういう記事書いてるぐらいなんで、なんか困ったことあったら聞いてください。
さっきマシュマロ開いたので聞いてください。
公開されるのが嫌な質問はDMでも良いです。

おわりに

煽りっぽいタイトルにしてしまったのですが、個人的にはRust入門者が躓かない為を思って書いた記事になります。

もちろん、この記事は個人の感想に基づいてしか書かれていない記事になりますので参考程度にと思っていますが、是非非非同期 Rustで快適なRust入門ライフを楽しんでもらえたらと思います。


以下補足情報

言葉の定義

非同期Rust(async Rust)

この記事の中の定義として、async, awaitと非同期ランタイムを用いたRustプログラムを指します。
std::threadrayon等を利用したRustはここでは非同期Rustとは呼びません。(同期Rustと呼ぶのも微妙なので、非非同期Rustと呼びます)

図で表すとこのような感じのイメージで思っておいてください。

Image from Gyazo

Rust初級者

この記事では、Rust初級者をRustの静的検証でおける課題に対して9割がたの問題に対してコンパイラの力を借りながら対処出来る人。
というぐらいの想定の定義にしてあります。

LLMの利用について

  • 記事本文に書かれている日本語はLLMは使わずに筆者が100%自分で書いた物になります。(推敲やアイディア出しなどにはLLMを利用しています)
  • 記事注釈に書かれている日本語はLLMが出力した物が含まれます。
  • 記事中の図にはLLMに生成させたものが含まれます。
  • 記事中のコードはLLMが生成ものに対して、適切な処理(コンパイルチェック等)を加えた物になります。

金言集

この記事に対しての反応や非同期Rust周りの話で頷くしかないやつを貼っておきます。
補足説明を付けすぎると、元ツイートの内容からズレる可能性があるため出来るだけ付加情報を付けないで掲載してます。

非同期Rustはコンパイラ側でも改善の途中

https://x.com/garasubo/status/1911413638193103097

非同期Rustで具体的に初学者がどのような詰まっているかがまとまっているページがある

https://x.com/helloyuki_/status/1911565435734221020

これの存在は僕は知らなかったのですが、50個のサブセクションあってかなりボリュームありました。

脚注
  1. https://rust-lang.github.io/rust-project-goals/2025h1/async.html ↩︎

  2. https://x.com/helloyuki_/status/1911568993611333861 ↩︎

  3. あと、この問題の根深いところはコンパイルエラー読みにくいというのもあります ↩︎

  4. 私は全く逆の印象を持っています。「あまたの問題に、数少ないルールで論理的に対処出来る言語」という印象です。 ↩︎

  5. かといって、Web Backendを同期Rustで書けるかと言うと、Web Backendを書くためのフレームワークは非同期Rustに強く依存しているため難しい状況と筆者は理解しています。(良いFWやライブラリがあれば教えてください。) ↩︎

  6. 非同期Rustは確かに強力ですが、C10K問題(1万以上の同時接続を処理する)のようなスケーラビリティが求められる特定のユースケースに特化しています。入門段階では、まずRustの基本的な概念(所有権、ライフタイム、トレイトなど)を理解してから、より複雑な非同期プログラミングに挑戦する方が学習曲線が緩やかになります。 ↩︎

  7. 特にコンパイラ、インタプリタは実装する言語が非同期タスクを処理したい場合は非同期Rust使う必要が出て来たりします。 ↩︎

  8. だめな例は、io bound(DBアクセスとか)で遅いのに特にプロファイルもせずにRustに書き換えることです。 ↩︎

  9. FFIで扱うunsafeはコンテキストがそこまで広くないのでunsafe比的には簡単な部類です。四天王の中でも最弱みたいな感じですかね。 ↩︎

Discussion

kanaruskanarus

なぜかわかりますか?
dataにSend + Sync が無いからです。

これ自体は単に tokio::spawnSend + Sync + 'static を要求しているからエラーになるというだけであって、「高度な抽象化」とか「非同期関数が内部的にどのような構造に変換されるか」というここの文脈とは直接関係ない話に見えるのですが、どうでしょうか?

higumachanhigumachan

コメントありがとうございます!

これ自体は単に tokio::spawn が Send + Sync + 'static を要求しているからエラーになるというだけであって

ここは、完全に正しいと理解しています。

「高度な抽象化」とか「非同期関数が内部的にどのような構造に変換されるか」というここの文脈とは直接関係ない話に見えるのですが、どうでしょうか?

ここは私の書きたかった問題の核の部分を取り違えてられていると感じました。

ここで言いたい問題は、なぜ tokio::spawnで呼ばれているprocess_dataという関数が前者では Send + Sync を持たず 後者では持っているのか?
というところになります。

理由は、async関数は { awaitを跨いで Send + Syncを持たない値を共有⇒ Send + Sync を持たない} からだと理解しています。(かなり、アバウトな理解をしている自覚はありますが、細かいところで間違っているかもしれません)

例えば、RC<RefCell<T>>を内部で持っていても await を跨いでいなければtokio::spawnから呼び出せてコンパイルが通ります

async fn process_data() {
    // Rc<RefCell<T>>はSend + Syncを実装していないが境界を跨いでいない。
    let mut result = {
        let data = Rc::new(RefCell::new(vec![1, 2, 3]));

        // 最初の同期処理
        let value = fetch_value();

        // dataはSend + Syncではないため、awaitの境界をまたいで使用できない
        data.borrow_mut().push(value);

        // 別の同期処理
        process_value(data.borrow().clone())
    };
    // 別の同期処理
    let other = some_other_async().await;

    result.push(other);

    println!("結果: {:?}", result);
}

つまり、このエラーは「単にtokio::spawnのtrait境界からの要請」のみの問題では無く、非同期関数がどのように内部変換され、その変換のされ方によって非同期関数のtrait 境界が変わる問題を指摘していたつもりでした。


これらの議論はあえて省いた部分でしたので誤解を与えたならばすいませんでした。
アコーディオンで追記しておきます。

kanaruskanarus

async関数は { awaitを跨いで Send + Syncを持たない値を共有⇒ Send + Sync を持たない} からだと理解しています。

あ、その話でしたか、なるほど!
( この理解は問題ないと思います ) edit: これは嘘。後で気づく

個人的には、この引用部分のような文言がないと、「非同期関数が内部的にどのような構造に変換されるか」といった文脈とこのサンプルコードのエラーの話は文章の中で論理的に繋がらず、読解し難いと感じました 。
今一度見直したところ、サンプルコードのコメントで「awaitの境界をまたいで使用できない」「awaitの境界をまたいで安全に使用できる」ということに言及されていることに気がつきました。
見落とし失礼しました 。。。

これらの議論 ~ アコーディオンで追記しておきます。

対応ありがとうございます。

一応

僕個人の文章感覚としては、「これらの議論」はこの文章の構成上必須の要素であり、追記どころか、本文中に組み込まれていなければならないと感じています。

( もちろん、この感覚がどれくらい一般的かは分からないので、対応はお任せします )

higumachanhigumachan

一応の指摘ありがとうございます。
こちらなのですが、
メイン読者をRust入門者(Rust本もくまなくは読んでいない)に置いているので本文中であんまり具体的なRustの問題に触れたくないというのはあります。

あくまで、

  • 非同期の合成は簡単そうに見えて難しい変換が入っている
  • その変換に起因した、一見すると謎のコンパイル通る/通らない差分がある

というのを伝えることをメインにしています。

もちろん、アコーディオンの外に出す方が議論の核は明確になりますが。
メイン読者からの「うわっ」感が増えるのはマイナスかと思って現在の対応にしています。


ただ、もう少し良さそうな書き方を思いついたので微修正します。

higumachanhigumachan
 :::
 
 なぜかわかりますか?
-`data`にSend + Sync が無いからです。
+
+それは、`async fn process_data` に `Send + Sync`が無いからです。
+なぜ、`async fn process_data` に `Send + Sync`が無いのか?
+それは、`data`にSend + Sync が無いからです。
 
 以下の様にすると[コンパイルは通るようになります。](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=7f6e9524f80c10e8176c3643a643a3ee)
 
 ```rust
+/// Send + Syncを持っている async fnの実装
 async fn process_data() {
     // Arc<Mutex<T>>はSend + Syncを実装している
     let data = Arc::new(Mutex::new(vec![1, 2, 3]));

こんな感じで書き換えました。

kanaruskanarus

再三の返信で恐縮です。
「await を跨いで...」に注目して「この理解は問題ないと思います」と書いたのですが、そういえばあれ本当か?と思って見直してみると、厳密には間違ってますね。。。
( 記事中の表現については、入門者向けということもあり、混乱させないことを優先してこのままでもいいかもしれませんが )

詳細

なぜ tokio::spawnで呼ばれているprocess_dataという関数が前者では Send + Sync を持たず 後者では持っているのか?

async関数は { awaitを跨いで Send + Syncを持たない値を共有⇒ Send + Sync を持たない} から

それは、async fn process_data に Send + Syncが無いからです。
なぜ、async fn process_data に Send + Syncが無いのか?
それは、dataにSend + Sync が無いからです。

という書き方をされていて、process_data という関数自体が Send かどうかという話をしているように読めるのですが、実際には await を跨いで Send + Sync を持たない値を共有していようが process_data そのものは Send です ( https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=a324bd1a5cc611c0d84410d7e8480add )

tokio::spawn のところで問題になっているのは process_data という関数(ポインタ) が Send かどうかではなく、process_data が返す ( Future を実装している ) opaque 型が Send かどうかです。

( 実際、わざわざ関数を用意せずに tokio::spawn(async { ... }) とベタ書きしても Send については全く同じことであり、async fn はこの話の本質ではないです )




これはコンパイル通りません[1]。

でリンクされている Playground でも、エラーメッセージは

   Compiling playground v0.0.1 (/playground)
error: future cannot be sent between threads safely
   --> src/main.rs:34:18
    |
34  |     tokio::spawn(process_data()).await.unwrap();
    |                  ^^^^^^^^^^^^^^ future returned by `process_data` is not `Send`
    |

となるはずで、future returned by process_data is not Send であって、process_data is not Send ではないです。

higumachanhigumachan

丁寧に指摘ありがとうございます!
おっしゃるとおりです。完全に間違えていました。

確かに、Sendが必要なのはprocess_dataの返り値であるFutureであってprocess_dataでは無いですね。
tokio::spawnも関数(ポインタ)では無くimpl Futureを受け取る物でした。

これは、さすがに記述を変更しないと大嘘記事になるので出来るだけ修正しました!(アコーディオンの中だけ、前提がぶれてしまっており免責いれて少し適当ですが)

以下が核の部分の変更になります。

差分
+そして、この関数の返り値は`some_function`を実行する為の状態機械を表した型になっており`Future` traitを実装しています。(訳わからない人は読み飛ばしてください)
+
 この抽象化は便利なのですが、Rustの知識が無いとすぐに牙を向いてきます。
 
 例えば以下のようなコードです。
 
+
 \`\`\`rust
 use std::rc::Rc;
 use std::cell::RefCell;
@@ -317,14 +327,14 @@ error: could not compile `playground` (bin "playground") due to 3 previous error
 
 なぜかわかりますか?
 
-それは、`async fn process_data` に `Send + Sync`が無いからです。
-なぜ、`async fn process_data` に `Send + Sync`が無いのか?
-それは、`data`にSend + Sync が無いからです。
+それは、`async fn process_data` の返り値に `Send + Sync`が無いからです。
+なぜ、`async fn process_data`の返り値に `Send + Sync`が無いのか?
+それは、`data`に`Send + Sync` が無いからです。
 
 以下の様にすると[コンパイルは通るようになります。](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=7f6e9524f80c10e8176c3643a643a3ee)
 
 \`\`\`rust
-/// Send + Syncを持っている async fnの実装
+/// 返り値がSend + Syncを持っている async fnの実装
 async fn process_data() {
     // Arc<Mutex<T>>はSend + Syncを実装している
     let data = Arc::new(Mutex::new(vec![1, 2, 3]));
@@ -351,22 +361,19 @@ async fn main() {
 }
 \`\`\`
lucidfrontier45lucidfrontier45

CPU boundは絶対条件

非常によくわかります。非同期Rustというか、そもそもほとんどDBとの通信がボトルネックのWebAPIサーバーをRustで書くことが正直やり過ぎに思え、多くのRust入門記事がWebAPIサーバーを扱っていることに違和感を感じます。

mars2nicomars2nico

じゃあどうすれば良いの?

tokioについて調べていてこの記事を見つけました。tokioではまだレイヤーが低いのかもなと感じました。tokioと依存関係にあってかつasyncブロックasync move {..}をうまく隠したライブラリ(クレート)を用いた開発なら入門しやすいのかもなと思いました。私自身Rust入門者なので詳細な議論については分かりません。投げやりですみません。

higumachanhigumachan

非同期Rustをする上では、asyncブロックを隠すというのはよくわからないと思いました。
そうしたいなら、同期Rustで良いじゃんって思っています。

Webサーバの文脈ならまだ話はわかって、各ハンドラがasync fnになっている物では無くて各リクエストの処理を行うときに単純なthreadでリクエストのハンドラ内はブロッキングの関数を使うみたいなアプローチは結構アリだとは思います。(PythonだとFlaskみたいなイメージですね)
ただ、じゃあ入門者向けのWeb Backend FWを誰が作るのか?みたいなのは難しいところだと思います。

可能性が現在ありそうなのは、https://loco.rs/ は今はaxumをベースにしていますが、非同期じゃ無くても良くてそれがユーザ獲得につながると思ったらやりそうみたいな感じぐらいですかね。

kanaruskanarus

async を使わないで coroutine を実現するということであれば ( tokio とは関係ないですが )

https://github.com/Xudong-Huang/may

というのがあります ( may_minihttp という簡易 Web Framework もあります )

Rust version of the popular Goroutine

を謳っていて、使ったことはないですが面白そうだなーと思っています


Webサーバの文脈ならまだ話はわかって、各ハンドラがasync fnになっている物では無くて各リクエストの処理を行うときに単純なthreadでリクエストのハンドラ内はブロッキングの関数を使う

じゃあ入門者向けのWeb Backend FWを誰が作るのか?

これは一応

https://github.com/tomaka/rouille

という Web Framework があって、

Async I/O, green threads, coroutines, etc. in Rust are still very immature.

The rouille library just ignores this optimization and focuses on providing an easy-to-use synchronous API instead, where each request is handled in its own dedicated thread.

とのことなので、まさにそういう思想のフレームワークです

mars2nicomars2nico

コメントありがとうございます。asyncを使わずに……という点についてもう少し具体的に考えをお伝えしたほうがいいのかなと思いました。

記事中のソースコードと対応していなくて恐縮ですが、イメージを伝えるためにプログラムを書いてみました。

サンプルソース
blocking_server_with_tokio_runtime.rs
use std::cell::RefCell;
use std::io;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::rc::Rc;

async fn handle_client(mut stream: TcpStream) -> io::Result<()> {
    let data = Rc::new(RefCell::new(vec![97u8, 97, 0]));
    let mut buf = [0; 1024];
    loop {
        let n = stream.read(&mut buf)?;
        if n == 0 {
            break;
        }
        // "aa"とレスポンスする
        let _ = stream.write(&data.borrow())?;
    }
    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listner = TcpListener::bind("127.0.0.1:8080")?;

    while let Ok((stream, _)) = listner.accept() {
        let _ = tokio::spawn(handle_client(stream));
    }

    Ok(())
}

一応、これでも、Rc::new(RefCell::new..を使って、ビルドは通りました。テストはしていません。

この記事でお伝えしたいこととは全く別の話題かもしれませんが、tokioは使いたいけどややこしい部分は避けたいしパフォーマンスもあまり重視していないという場合にはこういった書き方もあるのかもしれないなと私は思いました。

mars2nicomars2nico

とはいうものの、tokioを使いたいという考えが私の間違いかもしれないなと思いました。
c10k問題に対面しているわけではないので考えを改めてみます。ありがとうございました。

higumachanhigumachan

一応、これでも、Rc::new(RefCell::new..を使って、ビルドは通りました。テストはしていません。

それは、handle_clientの中でawaitしていないからですね。(状態機械として、1つのノードで構成されている)
このコードだとloopの中では一切Futureのスイッチが起きないので同期Rustと全く同じことをしているため、tokio::spawnしているが特に意味なくて、やっていることは同期Rustを書いているのと同じとなりますし、Runtimeの設定によっては著しくパフォーマンス下がります。

asyncを外して、tokio::spawn_blockingを使うのが良いと思います。同期関数を裏側で別スレッドで動かしてよしなにスレッドプール上で非同期関数化してくれます。

とはいうものの、tokioを使いたいという考えが私の間違いかもしれないなと思いました。

こちらに関しては、何がやりたいのかよくわかっていないのでわからないが私の回答です。

目的がTCPのserverを書きたいなら、threadベースでも出来ます。
目的がWeb serverを書きたいなら、非同期Rustになるけど axumとかactix-web使う感じかと思っています。(強い気持ちがあるなら、rouille使って見るのもありですが)

mars2nicomars2nico

やっていることは同期Rustを書いているのと同じ

そうですね。spawn_blockingを知らなかったです。これに変えます。

とはいうものの、tokioを使いたいという考えが私の間違いかもしれないなと思いました。

こちらに関しては、何がやりたいのかよくわかっていないのでわからないが私の回答です。

脈絡なく話をしてよく周りを困らせてしまうことがあって、よく周囲から言われます。すみません。目的はWeb serverを書くことです。

また、TCPのserverならこう、Web serverならこうと具体的にコメントいただきありがとうございました。
目的はWeb serverを書くことなのでaxumactic-webを読んでみることから始めるなど、参考にさせていただきます。