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の呼び出しをインライン化することによるコードブロートを嫌ったのだと推測していますが,正確な理由は分かりませんでした(識者求む) ↩