Background Tasks
Use case
You need long-running or periodic work that runs alongside your main logic: a worker loop, a heartbeat, or deferred jobs. In Rust you spawn tasks with tokio::spawn and coordinate with channels or shared state.
In C#: Like BackgroundService, IHostedService, or Task.Run with a long-running loop. In Rust there is no built-in hosted service; you spawn a future and keep the runtime alive.
Spawning a task
Use tokio::spawn(async { ... }) to run a future in the background. The spawn returns a JoinHandle; you can .await it to wait for completion or drop it to detach.
let handle = tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(2)).await;
println!("background done");
});
// do other work
let _ = handle.await;In C#: Like Task.Run(() => { ... }) or _ = Task.Run(...). In Rust, dropping the handle does not cancel the task; it keeps running.
Periodic work
Run a loop with tokio::time::interval and tick in the loop. Use interval.tick().await to wait for the next tick.
let mut interval = tokio::time::interval(Duration::from_secs(1));
loop {
interval.tick().await;
println!("tick");
}In C#: Like a Timer or PeriodicTimer; in Rust you use the interval in a loop.
Channels for work queues
Use tokio::sync::mpsc to send work items to a background task. The task loops on rx.recv().await and processes each item. The main code sends with tx.send(item).await.
In C#: Like Channel<T> or a BlockingCollection with a consumer task. In Rust, the receiver is async and yields when empty.
Common pitfalls
- The first
interval.tick().awaitreturns immediately.tokio::time::interval(d)fires its first tick at once (not afterd), soloop { interval.tick().await; work(); }runswork()immediately, then everyd. Calltick()once before the loop if you want to wait the period first. - Dropping a
JoinHandledoes not cancel the task. It keeps running detached; to stop it you need a shutdown signal (awatch/mpscchannel orCancellationToken) — dropping just gives up the ability to.awaitit. - Slow work shifts the schedule. By default a missed tick fires immediately to catch up;
interval.set_missed_tick_behavior(MissedTickBehavior::Delay)changes that.
In C#: PeriodicTimer.WaitForNextTickAsync() waits the period before the first tick —
the opposite of tokio's immediate first tick; and you cancel via a CancellationToken, not
by dropping the task.
Key takeaway
Use tokio::spawn for fire-and-forget or for joining later. Use tokio::time::interval for periodic work. Use mpsc channels to send work to a background loop. Keep the runtime alive (e.g. await the main future or the join handle).