0Pricing
Learn Rust Coding · Урок

Работа с будущими и задачами

Разберитесь в трейте `Future`, а также в том, как задачи планируются и управляются в асинхронной среде выполнения.

«Работа с будущими и задачами» — бесплатный урок Learn Rust Coding на CoddyKit. Это урок 3 из 3. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Learn Rust Coding, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Learn Rust Coding содержит 3 уроков всего.

Что такое Future в Rust

В асинхронном Rust Future — это трейт, представляющий асинхронное вычисление, которое может завершиться в некоторый момент. Представляйте его как обещание значения, которое ещё не готово.

  • Это основной строительный блок асинхронного Rust.
  • Функции async fn в Rust фактически возвращают анонимный тип, реализующий трейт Future.
  • Значение вычисляется не сразу, а в момент, когда исполнитель выполняет «опрос» (poll) Future.

Как фьючерсы продвигаются к завершению

Future не выполняется самостоятельно. Исполнитель (например, среда Tokio) многократно выполняет его «опрос», проверяя, продвинулось ли вычисление или завершилось ли оно.

  • При опросе Future возвращает либо Poll::Pending (ещё не завершено), либо Poll::Ready(T) (завершено, вот результат).
  • Если возвращено Pending, исполнитель знает, что нужно опросить его снова позднее, когда произойдёт что-то важное (например, завершится ввод-вывод).
  • Именно этот механизм опроса позволяет множеству асинхронных операций выполняться одновременно в одном потоке.

Ваш асинхронный код — это Future

Когда Вы пишете async fn, Rust преобразует её в конечный автомат, реализующий трейт Future. Фактическое вычисление начинается только после опроса возвращённого Future.

Рассмотрим простую асинхронную функцию async. Она не выполняется, пока исполнитель не дождётся её результата.

async fn say_hello() -> String {
    "Hello from a Future!".to_string()
}

#[tokio::main]
async fn main() {
    let future = say_hello(); // This doesn't run the function yet!
    println!("Future created, but not awaited.");
    // To run it, an executor needs to poll it, often via .await
    // let result = future.await;
    // println!("{}", result);
}

Фьючерсы становятся задачами

Если Future — это описание асинхронного вычисления, то задача — активный экземпляр этого Future, выполнение которого исполнитель доводит до завершения.

  • Когда Вы поручаете исполнителю (например, Tokio) выполнить Future, он оборачивает его в задачу.
  • Затем исполнитель управляет этой задачей, опрашивая её всякий раз, когда она готова продвинуться дальше.
  • Задачи — это единицы работы, которые асинхронная среда выполнения планирует и выполняет одновременно.

Запуск задач для параллельного выполнения

Чтобы выполнять Future одновременно с другим кодом, его «запускают» в среде Tokio. Это создаёт новую задачу, которой будет управлять среда выполнения.

Функция tokio::spawn принимает Future и возвращает JoinHandle, который можно использовать для ожидания завершения задачи и получения её результата.

async fn perform_task(id: u8) -> String {
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    format!("Task {} finished!", id)
}

#[tokio::main]
async fn main() {
    println!("Main started.");
    let handle = tokio::spawn(async {
        perform_task(1).await
    });
    println!("Task 1 spawned.");
    // We will await 'handle' in the next scene to get the result.
    // let result = handle.await.unwrap();
    // println!("{}", result);
}

Получение результатов задач

Возвращаемый функцией tokio::spawn объект JoinHandle сам является Future. Вы можете дождаться результата этого объекта с помощью .await, чтобы дождаться завершения запущенной задачи и получить её возвращаемое значение.

Если в запущенной задаче возникает паника, ожидание её JoinHandle вернёт ошибку.

async fn perform_task(id: u8) -> String {
    tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
    format!("Task {} finished!", id)
}

#[tokio::main]
async fn main() {
    println!("Main started.");
    let handle = tokio::spawn(async {
        perform_task(1).await
    });
    println!("Task 1 spawned.");
    let result = handle.await.unwrap(); // Await the handle to get the result
    println!("{}", result);
    println!("Main finished.");
}

Несколько параллельных задач

Преимущества задач и tokio::spawn особенно заметны при одновременном выполнении нескольких операций. Среда Tokio эффективно переключается между задачами по мере их готовности.

Это позволяет программе продвигаться сразу в нескольких направлениях без блокировки, даже используя один поток.

async fn long_task(id: u8) -> String {
    tokio::time::sleep(tokio::time::Duration::from_millis(100 + id as u64 * 50)).await;
    format!("Long task {} done!", id)
}

#[tokio::main]
async fn main() {
    println!("Main started.");
    let handle1 = tokio::spawn(long_task(1));
    let handle2 = tokio::spawn(long_task(2));
    println!("Both tasks spawned.");
    let result1 = handle1.await.unwrap();
    let result2 = handle2.await.unwrap();
    println!("{}\n{}", result1, result2);
    println!("Main finished.");
}

Ожидание всех задач с помощью join!

Когда нужно одновременно дождаться завершения нескольких Future, полезным макросом будет tokio::join!. Он одновременно ожидает все переданные фьючерсы и возвращает их результаты в виде кортежа.

Он похож на отдельное ожидание каждого обработчика, но обычно короче для фиксированного количества фьючерсов.

async fn fetch_data(source: &str) -> String {
    tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
    format!("Data from {}", source)
}

#[tokio::main]
async fn main() {
    println!("Starting data fetches...");
    let (data_a, data_b) = tokio::join!(
        fetch_data("Server A"),
        fetch_data("Database B")
    );
    println!("Fetched: {}\nFetched: {}", data_a, data_b);
    println!("All fetches complete.");
}

Обработка ошибок задач

Асинхронные операции могут завершаться ошибкой, как и синхронные. Часто Future возвращают тип Result, указывающий на успешное выполнение или ошибку.

Ожидая JoinHandle, помните, что результат обёрнут ещё в один Result, поскольку сама задача может завершиться паникой. Часто встречаются handle.await? (внутри async fn, возвращающей Result) или handle.await.unwrap() / .expect().

async fn might_fail(should_fail: bool) -> Result<String, &'static str> {
    tokio::time::sleep(tokio::time::Duration::from_millis(30)).await;
    if should_fail {
        Err("Oops, something went wrong!")
    } else {
        Ok("Operation successful!".to_string())
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let handle_ok = tokio::spawn(might_fail(false));
    let handle_err = tokio::spawn(might_fail(true));

    let result_ok = handle_ok.await??; // Await handle, then await inner Result
    println!("{}", result_ok);

    match handle_err.await? { // Await handle, then handle inner Result
        Ok(val) => println!("{}", val),
        Err(e) => eprintln!("Error: {}", e),
    }
    Ok(())
}

Понимание фьючерсов и задач

Вы узнали о фьючерсах как о вычислениях и о задачах как об экземплярах, выполняющих эти вычисления. Какое утверждение о tokio::spawn и JoinHandle верно?

Итоги: фьючерсы и задачи

Отличная работа! Теперь Вы освоили основные понятия Future и задач в асинхронной экосистеме Rust:

  • Future — это трейт, представляющий асинхронное вычисление, которое со временем выдаст значение.
  • Функции async fn компилируются в типы, реализующие трейт Future.
  • Задача — это экземпляр Future, которым исполнитель (например, Tokio) активно управляет и который опрашивает.
  • tokio::spawn используется для создания новой задачи и возвращает JoinHandle.
  • С помощью .await для JoinHandle можно получить результат запущенной задачи.

Эти строительные блоки необходимы для написания эффективных неблокирующих приложений на Rust!

Часто задаваемые вопросы

Урок «Работа с будущими и задачами» бесплатный?

Да — полный текст урока «Работа с будущими и задачами» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Learn Rust Coding, подпишись на CoddyKit PRO. Курс Learn Rust Coding содержит 3 уроков всего.

Чему я научусь в уроке «Работа с будущими и задачами»?

Разберитесь в трейте `Future`, а также в том, как задачи планируются и управляются в асинхронной среде выполнения. Ты практикуешь Learn Rust Coding с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Learn Rust Coding?

Предыдущий опыт не требуется. Learn Rust Coding на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 3.

Сколько времени занимает урок «Работа с будущими и задачами»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Learn Rust Coding?

Да. Каждый урок Learn Rust Coding включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Введение в Async/Await
  2. Создание асинхронных приложений с Tokio
  3. Работа с будущими и задачами
← Назад к Learn Rust Coding