🙅‍♂️

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

に公開
16

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を読んでみることから始めるなど、参考にさせていただきます。