hello tokio
Мы начнем с написания очень простого приложения на Tokio. Оно подключится к серверу Mini-Redis, установит значение ключа hello в world, а затем прочитает этот ключ обратно. Это будет сделано с использованием клиентской библиотеки Mini-Redis.
Код
Генерация нового крейта
Давайте начнем с создания нового Rust-приложения:
$ cargo new my-redis
$ cd my-redis
Добавление зависимостей
Затем откройте Cargo.toml и добавьте следующее прямо под [dependencies]:
tokio = { version = "1", features = ["full"] }
mini-redis = "0.4"
Написание кода
Затем откройте main.rs и замените содержимое файла на:
use mini_redis::{client, Result}; fn dox() { #[tokio::main] async fn main() -> Result<()> { // Открываем соединение с адресом mini-redis. let mut client = client::connect("127.0.0.1:6379").await?; // Устанавливаем ключ "hello" со значением "world" client.set("hello", "world".into()).await?; // Получаем ключ "hello" let result = client.get("hello").await?; println!("got value from the server; result={:?}", result); Ok(()) } }
Убедитесь, что сервер Mini-Redis запущен. В отдельном окне терминала выполните:
$ mini-redis-server
Если вы еще не установили mini-redis, вы можете сделать это с помощью:
$ cargo install mini-redis
Теперь запустите приложение my-redis:
$ cargo run
got value from the server; result=Some(b"world")
Успех!
Полный код можно найти здесь.
Разбор кода
Давайте подробнее разберем, что мы только что сделали. Кода немного, но происходит много всего.
#![allow(unused)] fn main() { use mini_redis::client; async fn dox() -> mini_redis::Result<()> { let mut client = client::connect("127.0.0.1:6379").await?; Ok(()) } }
Функция client::connect предоставляется крейтом mini-redis. Она асинхронно устанавливает TCP-соединение с указанным удаленным адресом. После установления соединения возвращается handle client. Хотя операция выполняется асинхронно, код, который мы пишем, выглядит синхронным. Единственное указание на то, что операция асинхронная — это оператор .await.
Что такое асинхронное программирование?
Большинство компьютерных программ выполняются в том же порядке, в котором они написаны. Сначала выполняется первая строка, затем следующая и так далее. При синхронном программировании, когда программа встречает операцию, которая не может быть завершена немедленно, она блокируется до завершения операции. Например, установление TCP-соединения требует обмена с узлом по сети, что может занять значительное время. В течение этого времени поток заблокирован.
При асинхронном программировании операции, которые не могут быть завершены немедленно, приостанавливаются и переносятся в фон. Поток не блокируется и может продолжать выполнять другие задачи. Как только операция завершается, задача возобновляется и продолжает обработку с того места, где она остановилась. В нашем предыдущем примере была только одна задача, поэтому ничего не происходит, пока она приостановлена, но асинхронные программы обычно имеют много таких задач.
Хотя асинхронное программирование может привести к более быстрым приложениям, оно часто приводит к гораздо более сложным программам. Программист должен отслеживать все состояние, необходимое для возобновления работы после завершения асинхронной операции. Исторически это была утомительная и подверженная ошибкам задача.
Зеленые потоки во время компиляции
Rust реализует асинхронное программирование с помощью функции под названием async/await. Функции, которые выполняют асинхронные операции, помечаются ключевым словом async. В нашем примере функция connect определена так:
#![allow(unused)] fn main() { use mini_redis::Result; use mini_redis::client::Client; use tokio::net::ToSocketAddrs; pub async fn connect<T: ToSocketAddrs>(addr: T) -> Result<Client> { // ... unimplemented!() } }
Определение async fn выглядит как обычная синхронная функция, но работает асинхронно. Rust преобразует async fn во время компиляции в процедуру, которая работает асинхронно. Любые вызовы .await внутри async fn возвращают управление обратно потоку. Поток может выполнять другую работу, пока операция обрабатывается в фоне.
предупреждение Хотя другие языки также реализуют
async/await, Rust использует уникальный подход. В первую очередь, асинхронные операции Rust ленивы. Это приводит к другой семантике выполнения по сравнению с другими языками.
Если это пока не совсем понятно, не волнуйтесь. Мы подробнее исследуем async/await в этом руководстве.
Использование async/await
Асинхронные функции вызываются как любые другие функции Rust. Однако вызов этих функций не приводит к выполнению тела функции. Вместо этого вызов async fn возвращает значение, представляющее операцию. Это концептуально аналогично замыканию без аргументов. Чтобы фактически выполнить операцию, вы должны использовать оператор .await для возвращаемого значения.
Например, данная программа
async fn say_world() { println!("world"); } #[tokio::main] async fn main() { // Вызов `say_world()` не выполняет тело `say_world()`. let op = say_world(); // Этот println! выводится первым println!("hello"); // Вызов `.await` на `op` начинает выполнение `say_world`. op.await; }
выводит:
hello
world
Возвращаемое значение async fn — это анонимный тип, реализующий трейт Future.
Асинхронная функция main
Главная функция, используемая для запуска приложения, отличается от обычной, встречающейся в большинстве крейтов Rust.
- Это
async fn - Она аннотирована с
#[tokio::main]
async fn используется, потому что мы хотим войти в асинхронный контекст. Однако асинхронные функции должны выполняться средой выполнения (runtime). Среда выполнения содержит планировщик асинхронных задач, предоставляет событийный I/O, таймеры и т.д. Среда выполнения не запускается автоматически, поэтому главная функция должна её запустить.
Функция #[tokio::main] — это макрос. Она преобразует async fn main() в синхронную fn main(), которая инициализирует экземпляр среды выполнения и выполняет асинхронную главную функцию.
Например, следующий код:
#[tokio::main] async fn main() { println!("hello"); }
преобразуется в:
fn main() { let mut rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { println!("hello"); }) }
Подробности среды выполнения Tokio будут рассмотрены позже.
Cargo features
При подключении Tokio для этого руководства был включен флаг функции full:
tokio = { version = "1", features = ["full"] }
Tokio имеет много функциональности (TCP, UDP, Unix-сокеты, таймеры, утилиты синхронизации, несколько типов планировщиков и т.д.). Не всем приложениям нужна вся функциональность. При попытке оптимизировать время компиляции или размер конечного приложения, приложение может выбрать только те функции, которые оно использует.