2019年11月にリリースされたRust 1.39では,非同期プログラミングを支援するためにasync/await
が言語機能として導入されました.
機能自体の解説は「Rustの非同期プログラミングをマスターする」などを読んでいただくと良いでしょう.
本稿ではasync/await
の基本的な知識は前提としたうえでformat!
との組み合わせにより生じる不可解なエラーについて解説します.
エラー例
次のコードはString
を受け取るasync fn
にformat!
で作成した文字列を渡し,await
しています.
async fn take_string(_: String) {}
fn main() {
tokio::spawn(async {
take_string(format!("")).await;
});
}
このコードをコンパイルすると
$ cargo c
Checking crate_name v0.1.0 (/path/to/crate)
error[E0277]: `*mut (dyn std::ops::Fn() + 'static)` cannot be shared between threads safely
--> src/main.rs:4:5
|
4 | tokio::spawn(async {
| ^^^^^^^^^^^^ `*mut (dyn std::ops::Fn() + 'static)` cannot be shared between threads safely
|
::: /home/user/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-0.2.0-alpha.6/src/executor.rs:100:40
|
100 | F: Future<Output = ()> + 'static + Send,
| ---- required by this bound in `tokio::executor::spawn`
|
= help: within `core::fmt::Void`, the trait `std::marker::Sync` is not implemented for `*mut (dyn std::ops::Fn() + 'static)`
= note: required because it appears within the type `std::marker::PhantomData<*mut (dyn std::ops::Fn() + 'static)>`
= note: required because it appears within the type `core::fmt::Void`
= note: required because of the requirements on the impl of `std::marker::Send` for `&core::fmt::Void`
= note: required because it appears within the type `std::fmt::ArgumentV1<'_>`
= note: required because it appears within the type `[std::fmt::ArgumentV1<'_>; 0]`
= note: required because it appears within the type `for<'r, 's, 't0, 't1, 't2, 't3, 't4, 't5, 't6, 't7, 't8> {[&'r str; 0], &'s [&'t0 str], &'t1 [&'t2 str; 0], (), [std::fmt::ArgumentV1<'t3>; 0], &'t4 [std::fmt::ArgumentV1<'t5>], &'t6 [std::fmt::ArgumentV1<'t7>; 0], std::fmt::Arguments<'t8>, std::string::String, impl std::future::Future, impl std::future::Future, ()}`
= note: required because it appears within the type `[static generator@src/main.rs:4:24: 6:6 for<'r, 's, 't0, 't1, 't2, 't3, 't4, 't5, 't6, 't7, 't8> {[&'r str; 0], &'s [&'t0 str], &'t1 [&'t2 str; 0], (), [std::fmt::ArgumentV1<'t3>; 0], &'t4 [std::fmt::ArgumentV1<'t5>], &'t6 [std::fmt::ArgumentV1<'t7>; 0], std::fmt::Arguments<'t8>, std::string::String, impl std::future::Future, impl std::future::Future, ()}]`
= note: required because it appears within the type `std::future::GenFuture<[static generator@src/main.rs:4:24: 6:6 for<'r, 's, 't0, 't1, 't2, 't3, 't4, 't5, 't6, 't7, 't8> {[&'r str; 0], &'s [&'t0 str], &'t1 [&'t2 str; 0], (), [std::fmt::ArgumentV1<'t3>; 0], &'t4 [std::fmt::ArgumentV1<'t5>], &'t6 [std::fmt::ArgumentV1<'t7>; 0], std::fmt::Arguments<'t8>, std::string::String, impl std::future::Future, impl std::future::Future, ()}]>`
= note: required because it appears within the type `impl std::future::Future`
と書いたコードよりも長いエラーが出てしまいました!
解決策
format!
の結果を変数に代入します.
async fn take_string(_: String) {}
fn main() {
tokio::spawn(async {
let s = format!("");
take_string(s).await;
});
}
するとコンパイルが通るようになります1.
具体的には,.await
式中に一時変数としてformat!
が存在しているとエラーになってしまいます.
最小の再現例
上のエラーは本質的には以下のコードによって再現されます.
async fn take_string(_: String) {}
// 引数がSendであることをチェック
fn is_send<T: Send>(_: T) {}
fn main() {
// format!を使うFutureを作成
let fut = async {
take_string(format!("")).await;
};
is_send(fut); // エラー: futがSendではない
}
関数is_send
は引数にSend
を要求するのですが,format!
と.await
とを組み合わせたせいでasync
ブロックがSend
を満たさなくなってしまいます(このことを!Send
であるともいいます).
tokio::spawn
なども同様に引数のFuture
がSend
であることを要求するため,例で挙げたようなasync
関数を渡すとエラーになってしまうのです.
Send
とは何か
それではなぜ非同期ランタイムはFuture
がSend
であることを要求するのでしょうか.また,Send
とはそもそも何なのでしょうか.
Rustはデータ競合からの安全性をコンパイル時にチェックしています.
例えば,Rc
は参照カウント方式のスマートポインタですが,参照カウントの操作でスレッド間同期を行っていないので,複数スレッドから同時に同じメモリを指すRc
を操作できてしまうと二重free
やデータ競合などの未定義動作を引き起こす可能性が生じてしまいます.
これをコンパイル時に防ぐためにRustは「スレッド間を跨いで移動できるデータ型」を表すトレイトSend
(とSync
)を導入しました.thread::spawn
などのスレッド間でデータを移動する可能性がある関数は引数型にSend
を要求し,Rc
のようなスレッドを跨ぐと危険な型はSend
を実装しないので,上で挙げたような危険な呼び出しを防ぐことができます.
また,Send
(やSync
)はauto traitと呼ばれており,基本的にはコンパイラによって自動的に実装されます.このとき,ある型がSend
であるためにはその型のフィールドはすべてSend
でなくてはいけません(ここ後で重要).ですから,Rc
を含む構造体は自動的に!Send
(Send
を満たさない)となります.
さて,非同期ランタイムがSend
を要求する理由に戻りましょう.一般的な非同期ランタイムはスレッドプールやワークスティーリングを用いて空いているスレッドにタスク(Future
)を割り当てることでスループットを上げる戦略をとっています.
つまり,別のスレッドにFuture
に移動させる可能性があるので,Future
がSend
を実装している必要があるのです.
ちなみに,スレッドプールを用いるようなランタイムでもSend
でないFuture
を実行するためのAPIを備えている場合があります.
行う処理の都合上Send
ではなくなってしまう場合は,このようなトレイト制約にSend
を持たないAPIを探しましょう.
なぜformat!
を使ったFuture
がSend
を実装しないのか
Future
がSend
である条件
ここまででは非同期ランタイムの関数がFuture
にSend
を要求することがあることおよびその理由を学びました.
それでは,なぜformat!
とawait
の組み合わせて作ったFuture
(上のコードのasync
ブロック)はSend
を実装しなくなってしまうのでしょうか.
それにはRustにおけるasync
がどのように働くのかを理解する必要があります.
Rustでは(JavaScriptやC#等でのasync/await
とは異なり)async
関数を呼び出しても処理の実行が始まりません(同様に,async
ブロックを書いても処理は動きません).
処理が実際に実行され始めるのはasync
関数の戻り値のFuture
をawait
してからとなります.
このasync
関数の戻り値のFuture
には「現在どこまで処理が進んだか」と「次の処理を進めるために必要なデータ」が格納されており,await
時にはこれらのデータを参照して処理を行います.
さて,「現在どこまで処理が進んだか」を表すにはどのawait
で処理を中断しているのかを表す整数のフラグがあれば十分です.では,「次の処理を進めるために必要なデータ」は何になるのでしょうか.
これは**await
を跨いで存在するローカル変数**となります.次のasync
関数を考えてみましょう.
async fn foo() {
let x = 10; // ここでxに代入した
some_future().await; // ここで処理を中断する可能性がある
println!("{}", x); // awaitを跨いでxを使用している
}
async
関数foo
が返すFuture
はsome_future().await;
のところで処理を中断する可能性があります.処理を中断するということはいったん関数のスコープから抜けてしまうということですから,関数のローカル変数x
を破棄しなければいけません.しかし,ここで破棄してしまうと処理を再開した際にx
の値が失われてしまい,その後のprintln!("{}", x);
が未定義動作となってしまいます.
Rustではこれを防ぐために,await
の時点でそのawait
を跨いで使われているローカル変数の値をFuture
に保存します.foo
の場合はfoo
が返すFuture
にx
の値を保管しておき,await
から復帰したときには保存しておいた値を用いるので正しくx
の値を表示することができます.
さあ,前の節で述べた「型がSend
であるには,すべてのフィールドがSend
でなくてはいけない」という事実を思い出しましょう.
foo
の例ではx
はただの整数型だったので,当然Send
です.では,x
がRc
のような!Send
な型の場合はどうなるでしょうか?このとき,foo
が返すFuture
にはx
の値を保存するためのフィールドが含まれますから,foo
の戻り値もx
の!Send
を引き継いでしまいます.つまり,async
によって生成されるFuture
をSend
にするにはawait
を跨ぐローカル変数をすべてSend
としなければいけません.
format!
は!Send
なテンポラリを生成する
話の核心にぐっと近づいてきました.残る疑問は「なぜformat!
を使うと!Send
なローカル変数がawait
を跨いでしまうのか」です.
これにはformat!
マクロの展開形式が大きく関わっています.次のformat!
式を見てみましょう.
format!("foo{}bar{}baz", 0, "hehe")
これを,cargo-expand
を使ってマクロ展開すると
format(Arguments::new_v1(&["foo", "bar", "baz"], &[&0, &"hehe"]))
となります(細部は省略しました).このコードで一番重要なのは
Arguments::new_v1(&["foo", "bar", "baz"], &[&0, &"hehe"])
の部分です.ここではArgumentsという構造体を生成しています.
Arguments
はフォーマット文字列と引数を格納する構造体です.ユーザコードからはformat_args!
によって生成可能で,上に登場したformat関数で文字列化したりwrite関数によってWrite
に出力したりすることができます.
Arguments::new_v1
はコンパイラによってのみ利用可能なArguments
のコンストラクタです.その引数を見てみると&["foo", "bar", "baz"]
とフォーマット引数を使わない部分の文字列と&[&0, &"hehe"]
という引数部分を受け取っており,確かにフォーマットに必要なデータを保持していることを確認することができます.
さて,上のコードで&[&0, &"hehe"]
と書いたのにお気づきでしょうか.0
と"hehe"
の型は違うのにも関わらず,ここではまとめて一つの配列を構築してしまっています.本当のマクロ展開ではunsafe
を使ってフォーマット引数の型を消去した配列を構築しています2.ここの引数の型を消去しているというのが問題のキモです.どのような型の引数をformat!
に渡しても,構築されるArguments
の型は同じになります.たとえ引数が!Send
であってもです.
これが意味することは,Arguments
型はSend
を実装しないということです.なぜならば,Arguments
は内部にSend
でない型を持つ可能性があり,そのことは型消去によって隠蔽されてしまいます.すなわち,並列安全性を保証するには最も保守的な判断,すなわちArguments
はSend
でないという判断を下さなければいけないのです.これは実際に起きた問題であり,昔のArguments
の実装ではSend
を満たしてしまっていたためにsafeなRustプログラムでデータ競合を引き起こすことができてしまいました.
Arguments
のこの!Send
性は実際には[&0, &"hehe"]
の型消去した配列が!Send
であることを引き継いでいます.
そしてこの配列はこの文のみで使われるテンポラリな値ですが,Rustにおいてテンポラリは文の末尾まで生存します.
つまり
take_string(format!("")).await;
と記述したとき,
take_string(format(Arguments::new_v1(&[""], &[])))
.await; //^ここでフォーマット引数の配列が生成され
//^ここで破棄される
とコンパイラによって解釈されます.つまり,フォーマット引数の配列がawait
を跨いで使用されると判定され,生成されるFuture
に含まれてしまいます.結果,そのFuture
自体も配列の!Send
を引き継いで!Send
となってしまいます.
この仕組みを考えるとなぜformat!
の結果をlet
で受けるとエラーが消えるのかも分かります.
let s = format!("");
take_string(s).await;
とコードを書き直したとき,これはformat!
マクロを展開すると
let s = format(Arguments::new_v1(&[""], &[])) ;
take_string(s).await; //^生成 ^破棄
となり,フォーマット引数の配列はlet
文の末尾で破棄されます.つまり,この配列はawait
が来る前に寿命を迎えるのでFuture
のSend
性には影響を及ぼさないのです.
今後の展望
この記事で扱ったformat!
とasync/await
の組み合せの問題は言語設計のコーナーケースを踏んでしまったとも言えます.
format!
単体で見た際にはArguments
が!Send
であることは安全性の保証のために必須ですし,それが問題を及ぼすことはありませんでした.
しかし,async/await
と組み合せたことによってこの詳細な実装の都合が現れてきてしまい,その結果として極めて分かりづらいエラーに繋がってしまいました.
Rustコミュニティもこの問題を認識しており,Rust 1.41で解決される見込みです.この記事では最後にどのような解決方法が提案されたかを紹介します.
1. format!
の展開を変える
let
の導入によってFuture
をSend
にする方法を応用すると,次のように書くこともできます.
receive_string({
let s = format!(""); // ←この行で!Sendな配列が破棄されるのでawaitを跨がない
s // sがブロックの結果の値となり,関数の引数として渡される
}).await;
これを一歩進めて,format!
の展開自体を
{
let s = format(Arguments::new_v1(...));
s
}
としてあげると冒頭のコード例でも問題なくコンパイルが通ります.この変更はテクニカルには破壊的(振る舞いを変えるプログラムが存在する)のですが,問題ないと判断され既にマージされています.
ですので,Rust 1.41(もしくはNightly)からはもはやformat!
の問題は存在しません.
2. 配列のライフタイムを短くする
今回の問題の原因は[&0, &"hehe"]
といったフォーマット引数の配列がawait
を跨いで生存すると扱われていたことでした.
ですが,await
から復帰したあとのこの配列への操作はdrop
だけですし,この配列は参照の配列なのでそのdrop
も特に何もしません.
つまり,Rustコンパイラが不必要に長く配列のライフタイムを推論した結果エラーになったと考えることもできます.
ですので,コンパイラがより精密にライフタイムを扱うことで解決できるかもしれません.
-
エラーの内容でググってきた人はここでブラウザバックして大丈夫です ↩
-
ここの型消去はRust 1.0以前に
format!
(の前身)が初めて導入されたときから行われていました.おそらくDisplay::fmt
やDebug::fmt
の呼び出しをインライン化することによるコードブロートを嫌ったのだと推測していますが,正確な理由は分かりませんでした(識者求む) ↩