Почему tokio::spawn требует отправки для локальных переменных

мой тестовый код:

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
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
1
0
55
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы правы. Это не обязательно для надежности. Просто компилятор слишком строг.

tokio::spawn() требует, чтобы будущее было Send, а будущее не таково Send, если оно имеет какое-либо значение, отличное от Send, в .await точках. Но что нам действительно нужно, так это еще один автоматический признак, а не Send, назовем его SendNoEscape, который будет реализован для Rc (потому что, если он не ускользнет из будущего, разумно будет перемещать его между потоками), а не реализован для, скажем, MutexGuard (это действительно должно оставаться в той же теме). К сожалению, такой черты не существует и, возможно, никогда не будет.

Акцент @Cerberus: значения, полученные асинхронным кодом (включая параметры async fn), все равно должны реализовывать Send (поскольку в других потоках могут быть копии); но значения, созданные в асинхронном коде, могут быть SendNoEscape. Компилятор может легко это отследить.

«перемещать между потоками разумно» — нет. Если мы сохраним один клон Rc в одном потоке и переместим другой клон того же Rc (который на уровне типа неотличим от любого другого Rc) в другой поток, у нас может возникнуть гонка данных по его счетчикам. Вот что Rc: !Send мешает.

Cerberus 19.08.2024 18:16

@Cerberus Вот только ты не можешь. Попробуйте это сделать.

Chayim Friedman 19.08.2024 18:58

Правильный ответ на вопрос заключается в том, что код исправен, но компилятор не может узнать об этом. Этот ответ подразумевает, что существует метод вместе с признаком, который компилятор мог бы разумно использовать для определения безопасности этого кода, но я не думаю, что это разумно. Если я ошибаюсь, дайте пожалуйста более подробную информацию.

true equals false 19.08.2024 22:12

@trueequalsfalse Но компилятор может обнаружить это, как я уже сказал, с помощью дополнительного автоматического признака.

Chayim Friedman 19.08.2024 22:35

@trueequalsfalse Если рассказать об этом подробнее: блок/функция async не является некорректной только потому, что она содержит Rc поперек .await, а это означает, что мы могли бы реализовать еще один автоматический признак для Rc.

Chayim Friedman 19.08.2024 22:36

Тогда я предполагаю, что эта черта будет автоматически реализована для всех фьючерсов асинхронных блоков без клонирования Rc? Вопрос в том, как компилятор узнает, какие методы следует считать запрещенными внутри асинхронного блока, чтобы он мог реализовать эту особенность. Будет ли разрешен только Deref? Кроме того, если клон недоступен, какой смысл использовать Rc. Единственное применение, которое я могу придумать, уже описано в чем-то вроде этого. Мне также интересно, как это будет работать для типов, отличных от Rc.

true equals false 19.08.2024 22:46

@trueequalsfalse Клонирование Rc — это нормально. Вы можете хранить несколько его клонов в одном асинхронном блоке, поскольку все они вместе перейдут в новый поток.

Chayim Friedman 19.08.2024 23:14

«нет async блока/функции некорректно только потому, что он удерживает Rc через .await» - но чем «удержание .await» отличается (с точки зрения потокобезопасности) от простого «перехода в thread::spawn», за исключением, возможно, перемещения между потоками а не гарантированный? А переходить Rc в thread::spawn неразумно (опять же, поэтому !Send).

Cerberus 20.08.2024 02:20

@Cerberus Разница в том, что с thread::spawn() вы (вызывающая сторона) уже можете хранить клоны Rc, и теперь вы делитесь ими с другими потоками, что неразумно. Но с асинхронными блоками никакой другой код не может содержать клоны Rc, только этот асинхронный блок. Это означает, что если этот асинхронный блок переместит поток, все клоны этого Rc переместятся в тот же поток. По сути, это то же самое, что и OnlyOneCloneRc, который можно перемещать между потоками, за исключением того, что здесь у нас есть несколько клонов, но мы гарантируем, что все они будут перемещены вместе. Подумайте об этом хорошенько, и вы поймете, что это правда.

Chayim Friedman 20.08.2024 02:32

Итак, вы предлагаете, чтобы значения, захваченные асинхронным блоком и созданные им, обрабатывались по-разному (первое должно быть Send, а второе должно быть только SendNoEscape)?

Cerberus 20.08.2024 02:34

@Цербер Да, действительно. Но это вполне осуществимо для компилятора.

Chayim Friedman 20.08.2024 02:34

@Cerberus Я отредактировал ответ, чтобы прояснить это.

Chayim Friedman 20.08.2024 02:37

@Cerberus На самом деле, теперь, когда я об этом думаю, компилятор не может точно это отследить: значение, созданное в асинхронном блоке, может происходить из значений, захваченных асинхронным блоком. Поэтому в целях безопасности асинхронный блок должен быть не-Send, если есть какое-либо не-Send захваченное значение, даже если оно отброшено до первого .await. На данный момент это не критическое изменение: компилятор не отслеживает, отбрасывается ли значение перед .await каким-либо иным способом, кроме блоков, и нет способа заблокировать захват...

Chayim Friedman 20.08.2024 02:42

@Cerberus ...Однако, если быть точнее, планы есть (github.com/rust-lang/rust/issues/69663, но это не всё). Если они произойдут, это станет переломным изменением.

Chayim Friedman 20.08.2024 02:43

Другие вопросы по теме