advanced⏱90 minutosLección 4 de 10
Async/Await y Tokio
Domine la sintaxis async/await de Rust, el runtime Tokio, tareas asíncronas y construcción de aplicaciones asíncronas concurrentes
Async/Await y Tokio
El Rust asíncrono permite I/O concurrente con sobrecarga mínima. A diferencia de los hilos de SO, las tareas asíncronas son ligeras y multiplexadas en pocos hilos.
¿Por qué Async?
| Enfoque | Costo por Tarea | Máx. Tareas | Utilización de CPU |
|---|---|---|---|
| Hilo de SO | ~2 MB de pila | Miles | Apropiativo |
| Green thread | Variable | Muchas | Apropiativo |
| Rust async | ~pocos bytes | Millones | Cooperativo |
rust
// Hilos — costo pesado por tarea
use std::thread;
fn thread_example() {
for i in 0..100 {
thread::spawn(move || {
// Cada hilo: ~2 MB de pila, overhead de syscall
println!("{i}");
});
}
}
// Async — ligero
async fn async_example(i: usize) {
// Cada tarea: pequeña máquina de estados, sin pila
println!("{i}");
}Note
Async es ideal para cargas de trabajo ligadas a I/O (servidores web, bases de datos, servicios de red). Para trabajo vinculado a CPU, usa hilos.
Sintaxis Async/Await
rust
use std::time::Duration;
// Definir una función async
async fn do_something() -> u32 {
42
}
// Await dentro de una función async
async fn process() {
let result = do_something().await;
println!("{result}");
}
// Leer un archivo asíncronamente
async fn read_file(path: &str) -> String {
tokio::fs::read_to_string(path).await.unwrap()
}Qué son Realmente las Funciones Async
rust
// Esta función async:
async fn add(a: i32, b: i32) -> i32 {
a + b
}
// Se desazucara a:
fn add(a: i32, b: i32) -> impl Future<Output = i32> {
async move { a + b }
}El Runtime Tokio
Tokio es el runtime async de facto para Rust:
toml
[dependencies]
tokio = { version = "1", features = ["full"] }rust
#[tokio::main]
async fn main() {
println!("Hello from Tokio!");
}
// Equivalente a:
fn main() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
println!("Hello from Tokio!");
});
}Success
tokio::spawn — Tareas Concurrentes
rust
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
sleep(Duration::from_secs(1)).await;
"world"
});
println!("hello ");
let result = handle.await.unwrap();
println!("{result}"); // "hello world"
}Múltiples Tareas Concurrentes
rust
use tokio::time::{sleep, Duration};
async fn task(name: &str, secs: u64) {
println!("{name} starting");
sleep(Duration::from_secs(secs)).await;
println!("{name} done");
}
#[tokio::main]
async fn main() {
let mut handles = vec![];
for i in 0..5 {
handles.push(tokio::spawn(task(&format!("task-{i}"), i)));
}
// Espera todas las tareas
for handle in handles {
handle.await.unwrap();
}
// O usa la macro join!:
tokio::join! {
task("a", 1),
task("b", 2),
task("c", 3),
};
}Redes Asíncronas con Tokio
rust
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
loop {
let (mut socket, addr) = listener.accept().await?;
println!("New connection from {addr}");
tokio::spawn(async move {
let mut buf = [0; 1024];
match socket.read(&mut buf).await {
Ok(n) if n == 0 => return,
Ok(n) => {
if let Err(e) = socket.write_all(&buf[..n]).await {
eprintln!("write error: {e}");
}
}
Err(e) => eprintln!("read error: {e}"),
}
});
}
}Canales en Tokio
Tokio proporciona canales asíncronos:
rust
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(32); // Canal con buffer
let producer = tokio::spawn(async move {
for i in 0..10 {
tx.send(i).await.unwrap();
}
});
let consumer = tokio::spawn(async move {
while let Some(msg) = rx.recv().await {
println!("Got: {msg}");
}
});
producer.await.unwrap();
consumer.await.unwrap();
}| Canal | ¿Con Buffer? | Caso de Uso |
|---|---|---|
mpsc | Ambos | Múltiples productores, consumidor único |
oneshot | No (un valor) | Comunicación única |
broadcast | Sí | Fan-out a muchos receptores |
watch | Sí | Notificaciones de cambio de estado |
Mutex Asíncrono
¡Nunca mantengas un std::sync::Mutex a través de .await! Usa tokio::sync::Mutex:
rust
use tokio::sync::Mutex;
use std::sync::Arc;
#[tokio::main]
async fn main() {
let counter = Arc::new(Mutex::new(0u32));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(tokio::spawn(async move {
let mut val = counter.lock().await;
*val += 1;
}));
}
for handle in handles {
handle.await.unwrap();
}
println!("Result: {}", *counter.lock().await); // 10
}Warning
Cancelación y Timeouts
rust
use tokio::time::{sleep, timeout, Duration};
#[tokio::main]
async fn main() {
// Timeout
let result = timeout(Duration::from_secs(1), async {
sleep(Duration::from_secs(2)).await;
"done"
}).await;
match result {
Ok(val) => println!("{val}"),
Err(_) => println!("timed out"),
}
// Select — el primero en completar gana
tokio::select! {
_ = sleep(Duration::from_secs(1)) => println!("one second"),
_ = sleep(Duration::from_secs(2)) => println!("two seconds"),
}
}Ejemplo Real: Cliente HTTP Asíncrono
rust
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
async fn fetch_url(host: &str, path: &str) -> Result<String, Box<dyn std::error::Error>> {
let mut stream = TcpStream::connect(format!("{host}:80")).await?;
let request = format!("GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n");
stream.write_all(request.as_bytes()).await?;
let mut reader = BufReader::new(stream);
let mut response = String::new();
reader.read_line(&mut response).await?;
Ok(response)
}
async fn fetch_all() -> Result<(), Box<dyn std::error::Error>> {
let urls = vec![
("example.com", "/"),
("httpbin.org", "/get"),
];
let mut handles = vec![];
for (host, path) in urls {
handles.push(tokio::spawn(async move {
match fetch_url(host, path).await {
Ok(body) => println!("{host}: {body}"),
Err(e) => eprintln!("{host} error: {e}"),
}
}));
}
for handle in handles {
handle.await?;
}
Ok(())
}
#[tokio::main]
async fn main() {
fetch_all().await.unwrap();
}Preguntas de Práctica
- ¿Cuál es la diferencia entre tareas async y hilos de SO?
- ¿Cómo difiere
async fnde una función regular? - ¿Qué papel juega Tokio en el Rust asíncrono?
- ¿Cómo crear una tarea asíncrona concurrente?
- ¿Cuál es la diferencia entre
tokio::join!ytokio::spawn? - ¿Por qué no debes mantener un
std::sync::Mutexa través de un.await? - ¿Cómo funciona
tokio::select!? - ¿Para qué sirve el azúcar sintáctico del atributo
#[tokio::main]? - ¿Cómo manejar timeouts en código async?
- ¿Qué tipos de canal Tokio existen y cuándo usar cada uno?
Progreso40%
Contenido del Curso
1. Hilos — spawn, join y Send + Sync2. Paso de Mensajes — Canales y Mutex Básico3. Estado Compartido — Mutex, RwLock, Arc y Atómicos4. Async/Await y Tokio5. Unsafe Rust6. FFI e Interoperabilidad con C7. Macros Declarativas (macro_rules!)8. Macros Procedurales9. Smart Pointers — Box, Rc, RefCell y Mutabilidad Interior10. Capstone Avanzado — Almacenamiento Clave-Valor Concurrente