Dart Async: Event Loop, Future, Stream, and Isolate
Dart is fundamentally a single-threaded language. While this might sound restrictive, it is a deliberate architectural decision to eliminate the complex race conditions and deadlocks common in traditional shared-memory multi-threading. Instead of multiple threads fighting over memory, Dart manages high-performance asynchronous operations through an Event Loop.
1. The Event Loop Architecture
Think of the Main Thread as a single "Waiter" in a restaurant. The waiter doesn't stay in the kitchen waiting for water to boil (I/O). Instead, they take an order (register a task), move on to serve another customer, and only return to "deliver the dish" (execute the callback) when the kitchen signals that it's ready.
The Two Queues
To manage priorities, the Event Loop maintains two distinct queues:
- MicroTask Queue (High Priority): Used for urgent internal system tasks and
Future.thencallbacks. The Event Loop will drain the entire MicroTask queue before it ever looks at the next standard event. - Event Queue (Standard Priority): Used for I/O operations, network requests, timers, and UI gestures. One event is processed at a time, followed by another check of the MicroTask queue.
Execution Flow: Synchronous Code → Full MicroTask Queue → ONE Event from Event Queue → Repeat.
2. Future: A One-Time Result
A Future<T> represents a value that will be provided at some point in the future.
- Non-Blocking
await: When youawaita Future, the current function "pauses" and yields control back to the Event Loop. The remaining code in the function is scheduled as a callback in the MicroTask queue to be executed once the Future completes. Future.wait([]): If you have multiple independent network calls, neverawaitthem sequentially. UseFuture.waitto trigger them concurrently, ensuring the total wait time is only as long as the slowest individual call.
3. Stream: Continuous Asynchronous Sequences
While a Future provides a single result, a Stream is an asynchronous iterator that provides a sequence of events over time (like a chat feed or a WebSocket connection).
async*&yield: These keywords allow you to create "Asynchronous Generators" that push multiple values to a subscriber over a period of time.- Single-Subscription vs. Broadcast: Most streams are single-subscription (optimized for a single listener). For events that multiple parts of your app need to hear (e.g., location changes), use
asBroadcastStream().
4. Isolate: True Multicore Parallelism
Asynchronous code (Future/Stream) handles I/O-bound tasks perfectly. However, CPU-bound tasks—such as parsing a 10MB JSON file or applying an image filter—will still block the single Main Thread, causing your UI to freeze (jank).
The solution is the Isolate:
- Shared-Nothing Memory: Unlike traditional threads, Isolates do not share memory. Each Isolate has its own private heap. This makes them "thread-safe" by design, as there is no shared state to protect with locks.
- Port-Based Communication: Isolates communicate purely by passing messages.
Concurrency vs. Parallelism
Future(Concurrency): Creates the illusion of multitasking by switching between tasks on a single core. Perfect for I/O.Isolate(Parallelism): Executes tasks simultaneously on separate CPU cores. Mandatory for heavy computation.
Optimization Tip: In Flutter, use the
computefunction to offload heavy calculations. It handles the manual work of spawning, communicating with, and killing an Isolate for a specific one-off task.