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 byprocess_data is not Send であって、process_data is not Send ではないです。
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.
Discussion
これ自体は単に
tokio::spawnがSend + Sync + 'staticを要求しているからエラーになるというだけであって、「高度な抽象化」とか「非同期関数が内部的にどのような構造に変換されるか」というここの文脈とは直接関係ない話に見えるのですが、どうでしょうか?コメントありがとうございます!
ここは、完全に正しいと理解しています。
ここは私の書きたかった問題の核の部分を取り違えてられていると感じました。
ここで言いたい問題は、なぜ tokio::spawnで呼ばれている
process_dataという関数が前者ではSend + Syncを持たず 後者では持っているのか?というところになります。
理由は、async関数は { awaitを跨いで
Send + Syncを持たない値を共有⇒Send + Syncを持たない} からだと理解しています。(かなり、アバウトな理解をしている自覚はありますが、細かいところで間違っているかもしれません)例えば、
RC<RefCell<T>>を内部で持っていてもawaitを跨いでいなければtokio::spawnから呼び出せてコンパイルが通ります。つまり、このエラーは「単にtokio::spawnのtrait境界からの要請」のみの問題では無く、非同期関数がどのように内部変換され、その変換のされ方によって非同期関数のtrait 境界が変わる問題を指摘していたつもりでした。
これらの議論はあえて省いた部分でしたので誤解を与えたならばすいませんでした。
アコーディオンで追記しておきます。
あ、その話でしたか、なるほど!
(
この理解は問題ないと思います) edit: これは嘘。後で気づく個人的には、この引用部分のような文言がないと、「非同期関数が内部的にどのような構造に変換されるか」といった文脈とこのサンプルコードのエラーの話は文章の中で論理的に繋がらず、読解し難いと感じました 。今一度見直したところ、サンプルコードのコメントで「awaitの境界をまたいで使用できない」「awaitの境界をまたいで安全に使用できる」ということに言及されていることに気がつきました。
見落とし失礼しました 。。。
対応ありがとうございます。
一応
僕個人の文章感覚としては、「これらの議論」はこの文章の構成上必須の要素であり、追記どころか、本文中に組み込まれていなければならないと感じています。
( もちろん、この感覚がどれくらい一般的かは分からないので、対応はお任せします )
一応の指摘ありがとうございます。
こちらなのですが、
メイン読者をRust入門者(Rust本もくまなくは読んでいない)に置いているので本文中であんまり具体的なRustの問題に触れたくないというのはあります。
あくまで、
というのを伝えることをメインにしています。
もちろん、アコーディオンの外に出す方が議論の核は明確になりますが。
メイン読者からの「うわっ」感が増えるのはマイナスかと思って現在の対応にしています。
ただ、もう少し良さそうな書き方を思いついたので微修正します。
こんな感じで書き換えました。
再三の返信で恐縮です。
「await を跨いで...」に注目して「この理解は問題ないと思います」と書いたのですが、そういえばあれ本当か?と思って見直してみると、厳密には間違ってますね。。。
( 記事中の表現については、入門者向けということもあり、混乱させないことを優先してこのままでもいいかもしれませんが )
詳細
という書き方をされていて、
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はこの話の本質ではないです )でリンクされている Playground でも、エラーメッセージは
となるはずで、future returned by
process_datais notSendであって、process_datais notSendではないです。丁寧に指摘ありがとうございます!
おっしゃるとおりです。完全に間違えていました。
確かに、
Sendが必要なのはprocess_dataの返り値であるFutureであってprocess_dataでは無いですね。tokio::spawnも関数(ポインタ)では無く
impl Futureを受け取る物でした。これは、さすがに記述を変更しないと大嘘記事になるので出来るだけ修正しました!(アコーディオンの中だけ、前提がぶれてしまっており免責いれて少し適当ですが)
以下が核の部分の変更になります。
差分
非常によくわかります。非同期Rustというか、そもそもほとんどDBとの通信がボトルネックのWebAPIサーバーをRustで書くことが正直やり過ぎに思え、多くのRust入門記事がWebAPIサーバーを扱っていることに違和感を感じます。
tokioについて調べていてこの記事を見つけました。tokioではまだレイヤーが低いのかもなと感じました。tokioと依存関係にあってかつasyncブロック
async move {..}をうまく隠したライブラリ(クレート)を用いた開発なら入門しやすいのかもなと思いました。私自身Rust入門者なので詳細な議論については分かりません。投げやりですみません。非同期Rustをする上では、asyncブロックを隠すというのはよくわからないと思いました。
そうしたいなら、同期Rustで良いじゃんって思っています。
Webサーバの文脈ならまだ話はわかって、各ハンドラがasync fnになっている物では無くて各リクエストの処理を行うときに単純なthreadでリクエストのハンドラ内はブロッキングの関数を使うみたいなアプローチは結構アリだとは思います。(PythonだとFlaskみたいなイメージですね)
ただ、じゃあ入門者向けのWeb Backend FWを誰が作るのか?みたいなのは難しいところだと思います。
可能性が現在ありそうなのは、https://loco.rs/ は今はaxumをベースにしていますが、非同期じゃ無くても良くてそれがユーザ獲得につながると思ったらやりそうみたいな感じぐらいですかね。
asyncを使わないで coroutine を実現するということであれば ( tokio とは関係ないですが )というのがあります ( may_minihttp という簡易 Web Framework もあります )
を謳っていて、使ったことはないですが面白そうだなーと思っています
これは一応
という Web Framework があって、
とのことなので、まさにそういう思想のフレームワークです
コメントありがとうございます。
asyncを使わずに……という点についてもう少し具体的に考えをお伝えしたほうがいいのかなと思いました。記事中のソースコードと対応していなくて恐縮ですが、イメージを伝えるためにプログラムを書いてみました。
サンプルソース
一応、これでも、
Rc::new(RefCell::new..を使って、ビルドは通りました。テストはしていません。この記事でお伝えしたいこととは全く別の話題かもしれませんが、tokioは使いたいけどややこしい部分は避けたいしパフォーマンスもあまり重視していないという場合にはこういった書き方もあるのかもしれないなと私は思いました。
とはいうものの、tokioを使いたいという考えが私の間違いかもしれないなと思いました。
c10k問題に対面しているわけではないので考えを改めてみます。ありがとうございました。
rouille よさげだなぁ。(10ヶ月前に更新が途切れていることだけが悲しい)
それは、handle_clientの中でawaitしていないからですね。(状態機械として、1つのノードで構成されている)
このコードだとloopの中では一切Futureのスイッチが起きないので同期Rustと全く同じことをしているため、tokio::spawnしているが特に意味なくて、やっていることは同期Rustを書いているのと同じとなりますし、Runtimeの設定によっては著しくパフォーマンス下がります。
asyncを外して、
tokio::spawn_blockingを使うのが良いと思います。同期関数を裏側で別スレッドで動かしてよしなにスレッドプール上で非同期関数化してくれます。こちらに関しては、何がやりたいのかよくわかっていないのでわからないが私の回答です。
目的がTCPのserverを書きたいなら、threadベースでも出来ます。
目的がWeb serverを書きたいなら、非同期Rustになるけど
axumとかactix-web使う感じかと思っています。(強い気持ちがあるなら、rouille使って見るのもありですが)そうですね。
spawn_blockingを知らなかったです。これに変えます。脈絡なく話をしてよく周りを困らせてしまうことがあって、よく周囲から言われます。すみません。目的はWeb serverを書くことです。
また、TCPのserverならこう、Web serverならこうと具体的にコメントいただきありがとうございました。
目的はWeb serverを書くことなので
axumやactic-webを読んでみることから始めるなど、参考にさせていただきます。