Для чего нужна асинхронность?

Все мы любим то, что Rust позволяет нам писать быстрые и безопасные приложения. Но для чего писать асинхронный код?

Асинхронный код позволяет нам запускать несколько задач параллельно в одном потоке ОС. Если вы ходите одновременно загрузить две разных web-страницы в обычном приложении, вы должны разделить работу между двумя разным потоками, как тут:


# #![allow(unused_variables)]
#fn main() {
fn get_two_sites() {
    // Spawn two threads to do work.
    let thread_one = thread::spawn(|| download("https:://www.foo.com"));
    let thread_two = thread::spawn(|| download("https:://www.bar.com"));

    // Wait for both threads to complete.
    thread_one.join().expect("thread one panicked");
    thread_two.join().expect("thread two panicked");
}
#}

Для многих приложений это замечательно работает - в конце концов, потоки были разработаны именно для этого: запускать несколько разных задач одновременно. Однако, они имеют некоторые ограничения. В процессе переключения между разными потоками и обменом данными между ними возникает много накладных расходов. Даже поток, который сидит и ничего не делает, использует ценные системные ресурсы. Асинхронный код предназначен для устранения этих проблем. Мы можем переписать функции выше используя Rust нотацию async/.await, которая позволяет нам запустить несколько задач одновременно, не создавая несколько потоков:


# #![allow(unused_variables)]
#fn main() {
async fn get_two_sites_async() {
    // Create a two different "futures" which, when run to completion,
    // will asynchronously download the webpages.
    let future_one = download_async("https:://www.foo.com");
    let future_two = download_async("https:://www.bar.com");

    // Run both futures to completion at the same time.
    join!(future_one, future_two);
}
#}

В целом, асинхронные приложения могут быть намного быстрее и использовать меньше ресурсов, чем соответствующая многопоточная реализация. Однако, есть и обратная сторона. Потоки изначально поддерживаются операционной системой и их использование не требует какой-либо специальной модели программирования - любая функция может создать поток и вызов функции, использующей поток, обычно так же прост, как вызов обычной функции. Тем не менее, асинхронные функции требует специальной поддержки со стороны языка или библиотек. В Rust, async fn создаёт асинхронную функцию, которая возвращает Future. Для выполнения тела функции, возвращённая Future должна быть завершена.

Важно помнить, что традиционные приложения с потоками могут быть вполне эффективными и предсказуемость Rust и небольшой объём памяти могут значить, что вы можете далеко продвинуться без использования async. Повышенная сложность асинхронной модели программирования не всегда стоит этого и важно понимать, когда ваше приложения будет лучше работать с использованием просто поточной модели.