мой тестовый код:
use tokio::task::yield_now;
use std::rc::Rc;
#[tokio::main]
async fn main() {
tokio::spawn(async {
let rc = Rc::new("hello");
println!("{}", rc);
yield_now().await;
});
}
Я не понимаю: поскольку задача Tokio выполняется только в одном потоке одновременно, не будет никаких гонок данных, связанных с локальной переменной rc, в нескольких потоках. Так почему же tokio::spawn требует отправки для локальных переменных?
компиляция не удалась:
Compiling playground v0.0.1 (/playground)
error: future cannot be sent between threads safely
--> src/main.rs:6:5
|
6 | / tokio::spawn(async {
7 | | let rc = Rc::new("hello");
8 | | println!("{}", rc);
... |
11 | | });
| |______^ future created by async block is not `Send`
|
= help: within `{async block@src/main.rs:6:18: 6:23}`, the trait `Send` is not implemented for `Rc<&str>`, which is required by `{async block@src/main.rs:6:18: 6:23}: Send`
note: future is not `Send` as this value is used across an await
--> src/main.rs:10:21
|
7 | let rc = Rc::new("hello");
| -- has type `Rc<&str>` which is not `Send`
...
10 | yield_now().await;
| ^^^^^ await occurs here, with `rc` maybe used later
note: required by a bound in `tokio::spawn`
--> /playground/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.39.2/src/task/spawn.rs:167:21
|
165 | pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
| ----- required by a bound in this function
166 | where
167 | F: Future + Send + 'static,
| ^^^^ required by this bound in `spawn`
error: could not compile `playground` (bin "playground") due to 1 previous error
Вы правы. Это не обязательно для надежности. Просто компилятор слишком строг.
tokio::spawn()
требует, чтобы будущее было Send
, а будущее не таково Send
, если оно имеет какое-либо значение, отличное от Send
, в .await
точках. Но что нам действительно нужно, так это еще один автоматический признак, а не Send
, назовем его SendNoEscape
, который будет реализован для Rc
(потому что, если он не ускользнет из будущего, разумно будет перемещать его между потоками), а не реализован для, скажем, MutexGuard
(это действительно должно оставаться в той же теме). К сожалению, такой черты не существует и, возможно, никогда не будет.
Акцент @Cerberus: значения, полученные асинхронным кодом (включая параметры async fn
), все равно должны реализовывать Send
(поскольку в других потоках могут быть копии); но значения, созданные в асинхронном коде, могут быть SendNoEscape
. Компилятор может легко это отследить.
@Cerberus Вот только ты не можешь. Попробуйте это сделать.
Правильный ответ на вопрос заключается в том, что код исправен, но компилятор не может узнать об этом. Этот ответ подразумевает, что существует метод вместе с признаком, который компилятор мог бы разумно использовать для определения безопасности этого кода, но я не думаю, что это разумно. Если я ошибаюсь, дайте пожалуйста более подробную информацию.
@trueequalsfalse Но компилятор может обнаружить это, как я уже сказал, с помощью дополнительного автоматического признака.
@trueequalsfalse Если рассказать об этом подробнее: блок/функция async
не является некорректной только потому, что она содержит Rc
поперек .await
, а это означает, что мы могли бы реализовать еще один автоматический признак для Rc
.
Тогда я предполагаю, что эта черта будет автоматически реализована для всех фьючерсов асинхронных блоков без клонирования Rc
? Вопрос в том, как компилятор узнает, какие методы следует считать запрещенными внутри асинхронного блока, чтобы он мог реализовать эту особенность. Будет ли разрешен только Deref? Кроме того, если клон недоступен, какой смысл использовать Rc
. Единственное применение, которое я могу придумать, уже описано в чем-то вроде этого. Мне также интересно, как это будет работать для типов, отличных от Rc
.
@trueequalsfalse Клонирование Rc
— это нормально. Вы можете хранить несколько его клонов в одном асинхронном блоке, поскольку все они вместе перейдут в новый поток.
«нет async
блока/функции некорректно только потому, что он удерживает Rc через .await
» - но чем «удержание .await
» отличается (с точки зрения потокобезопасности) от простого «перехода в thread::spawn
», за исключением, возможно, перемещения между потоками а не гарантированный? А переходить Rc
в thread::spawn
неразумно (опять же, поэтому !Send
).
@Cerberus Разница в том, что с thread::spawn()
вы (вызывающая сторона) уже можете хранить клоны Rc
, и теперь вы делитесь ими с другими потоками, что неразумно. Но с асинхронными блоками никакой другой код не может содержать клоны Rc
, только этот асинхронный блок. Это означает, что если этот асинхронный блок переместит поток, все клоны этого Rc
переместятся в тот же поток. По сути, это то же самое, что и OnlyOneCloneRc
, который можно перемещать между потоками, за исключением того, что здесь у нас есть несколько клонов, но мы гарантируем, что все они будут перемещены вместе. Подумайте об этом хорошенько, и вы поймете, что это правда.
Итак, вы предлагаете, чтобы значения, захваченные асинхронным блоком и созданные им, обрабатывались по-разному (первое должно быть Send
, а второе должно быть только SendNoEscape
)?
@Цербер Да, действительно. Но это вполне осуществимо для компилятора.
@Cerberus Я отредактировал ответ, чтобы прояснить это.
@Cerberus На самом деле, теперь, когда я об этом думаю, компилятор не может точно это отследить: значение, созданное в асинхронном блоке, может происходить из значений, захваченных асинхронным блоком. Поэтому в целях безопасности асинхронный блок должен быть не-Send
, если есть какое-либо не-Send
захваченное значение, даже если оно отброшено до первого .await
. На данный момент это не критическое изменение: компилятор не отслеживает, отбрасывается ли значение перед .await
каким-либо иным способом, кроме блоков, и нет способа заблокировать захват...
@Cerberus ...Однако, если быть точнее, планы есть (github.com/rust-lang/rust/issues/69663, но это не всё). Если они произойдут, это станет переломным изменением.
«перемещать между потоками разумно» — нет. Если мы сохраним один клон
Rc
в одном потоке и переместим другой клон того жеRc
(который на уровне типа неотличим от любого другогоRc
) в другой поток, у нас может возникнуть гонка данных по его счетчикам. Вот чтоRc: !Send
мешает.