On this page24 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. Concurrency vs Parallelism
- 2. Taking Turns on One Thread
- 3. The Event Loop in Detail
- 4. Scheduling a Microtask
- 5. Scheduling an Event
- 6. The Full Ordering Run
- 7. Microtasks Created Inside an Event
- 8. Microtasks Can Starve Events
- 9. Heavy CPU Work Freezes Async Code
- 10. Marking It async Does Not Help
- 11. Isolates: Dart's Answer
- 12. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 97 — Dart Concurrency Model
| Category | 16 — Concurrency & Isolates |
| Lecture | 097 of 140 |
| Estimated video length | 10 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
You can start many Futures and Streams at once, but you could not say exactly in which order their callbacks run. This lecture opens the event loop and shows its two queues, so you can predict every line.
Contents
Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways
Every output and error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2. Timing lines are approximate: a line such as
at 812 msshows a different number on every run and every computer.
Prerequisites
Students should already understand, from Lectures 087–096:
- the event loop and
Future.delayed(Lecture 087) Future,Future.value,Future(() => ...), and.then(Lecture 088)asyncandawait(Lecture 089)- importing
dart:async(Lecture 090) andFuture.wait(Lecture 091) - Streams and async generators (Lectures 092–096)
Connection With Previous Lectures
Lecture 087 described the event loop as "one thread, one queue", and then admitted that Dart really has two queues. Try predicting this:
void main() {
Future(() => print('Future'));
Future.value(1).then((_) => print('then'));
print('main');
}main
then
Futurethen was scheduled second but ran first. With one queue that is impossible. To
explain it — and to see why a heavy calculation still freezes async code — we need the real
model.
Learning Objectives
By the end of this lecture you will be able to:
- Explain the difference between concurrency and parallelism
- Describe the event loop step by step
- Distinguish the microtask queue from the event queue
- Schedule work with
scheduleMicrotask,Future.microtask,Future(), andTimer.run - Predict the output of a program that mixes all four
- Recognize how endless microtasks starve the event queue
- Prove with
Stopwatchthat heavy CPU work freezes async code - Explain why isolates are Dart's answer to heavy work
1. Concurrency vs Parallelism
Concurrency means several tasks are in progress at the same time. Parallelism means several tasks are running at the same instant.
Analogy — one cook making tea and toast is concurrent: kettle on, bread in, butter ready, pour tea. Only one pair of hands, but nothing waits idle. Two cooks in the kitchen is parallel.
Dart's async code is concurrency with one cook: one thread runs your code, switching between tasks only when one of them waits.
2. Taking Turns on One Thread
Two async functions look like they run together, but each await simply hands the thread
back to the event loop:
Future<void> task(String name) async {
for (var step = 1; step <= 3; step++) {
print('$name: step $step');
await Future.delayed(Duration.zero);
}
}
void main() {
task('Ali');
task('Sara');
}Ali: step 1
Sara: step 1
Ali: step 2
Sara: step 2
Ali: step 3
Sara: step 3Never are two lines running at the same moment. Ali runs until his await, then Sara runs
until hers, and so on.
3. The Event Loop in Detail
Dart keeps two queues of waiting work:
- the microtask queue — small, urgent jobs that must happen before anything else
- the event queue — everything else: timers,
Future(), finished file and network work
The loop follows fixed rules:
- Run
main()to the end. - Run every microtask, until the microtask queue is empty.
- Take one event from the event queue and run it.
- Go back to step 2. When both queues are empty, the program exits.
Within each queue, jobs run in the order they were added: first in, first out.
4. Scheduling a Microtask
scheduleMicrotask (from dart:async) adds a function to the microtask queue.
Future.microtask does the same, but also returns a Future:
import 'dart:async';
void main() {
Future(() => print('event'));
scheduleMicrotask(() => print('microtask 1'));
Future.microtask(() => print('microtask 2'));
print('main');
}main
microtask 1
microtask 2
eventThe event was added first, but rule 2 empties the microtask queue before rule 3 takes any event.
5. Scheduling an Event
Future(() => ...), Future.delayed(Duration.zero, ...), and Timer.run (also from
dart:async) all add a job to the event queue. Among themselves, they keep their order:
import 'dart:async';
void main() {
Future.delayed(Duration.zero, () => print('Future.delayed zero'));
Timer.run(() => print('Timer.run'));
Future(() => print('Future()'));
}Future.delayed zero
Timer.run
Future()6. The Full Ordering Run
Here is every scheduler in one program:
import 'dart:async';
void main() {
print('main: start');
Timer.run(() => print('event: Timer.run'));
Future(() => print('event: Future()'));
scheduleMicrotask(() => print('microtask: scheduleMicrotask'));
Future.microtask(() => print('microtask: Future.microtask'));
Future.value(1).then((_) => print('microtask: then on Future.value'));
print('main: end');
}main: start
main: end
microtask: scheduleMicrotask
microtask: Future.microtask
microtask: then on Future.value
event: Timer.run
event: Future()Line by line:
main: startandmain: end— rule 1: synchronous code always finishes first.scheduleMicrotask— the first job added to the microtask queue.Future.microtask— the second microtask, so it comes second.then on Future.value— the future was already complete, so.thenput its callback straight into the microtask queue, third in line. This is whythenbeatFuturein the Connection puzzle.Timer.run— microtasks are empty; rule 3 takes the first event.Future()— the next event.
7. Microtasks Created Inside an Event
Rule 4 means a microtask scheduled during an event runs before the next event:
import 'dart:async';
void main() {
Future(() {
print('Event 1 starts');
scheduleMicrotask(() => print('Microtask from event 1'));
print('Event 1 ends');
});
Future(() => print('Event 2'));
}Event 1 starts
Event 1 ends
Microtask from event 1
Event 2The microtask waited for event 1 to finish — nothing interrupts running code — but it jumped ahead of event 2.
8. Microtasks Can Starve Events
Because rule 2 runs microtasks until the queue is empty, a microtask that keeps scheduling another one blocks every event:
import 'dart:async';
void main() {
final clock = Stopwatch()..start();
Timer.run(() => print('Timer ran at ${clock.elapsedMilliseconds} ms'));
var count = 0;
void next() {
count++;
if (clock.elapsedMilliseconds < 300) {
scheduleMicrotask(next);
} else {
print('$count microtasks ran first');
}
}
scheduleMicrotask(next);
}7739249 microtasks ran first
Timer ran at 301 msThe timer was ready at 0 ms and waited 300 ms. Without the time limit it would wait forever. Keep microtasks short and rare.
9. Heavy CPU Work Freezes Async Code
Async code helps while the program is waiting. Real calculation keeps the single thread busy, so no queue moves. This function counts prime numbers — honest work, not a busy loop:
int countPrimes(int limit) {
var count = 0;
for (var n = 2; n <= limit; n++) {
var isPrime = true;
for (var d = 2; d * d <= n; d++) {
if (n % d == 0) {
isPrime = false;
break;
}
}
if (isPrime) count++;
}
return count;
}
void main() {
final clock = Stopwatch()..start();
Future.delayed(Duration(milliseconds: 100), () {
print('Reminder (due at 100 ms) ran at ${clock.elapsedMilliseconds} ms');
});
final primes = countPrimes(5000000);
print('Found $primes primes at ${clock.elapsedMilliseconds} ms');
}One run printed (approximate):
Found 348513 primes at 812 ms
Reminder (due at 100 ms) ran at 813 msThe reminder was 700 ms late. In an app, that is a frozen screen.
10. Marking It async Does Not Help
Wrapping the calculation in an async function changes nothing — the body still runs on the
same thread. Here a heartbeat should tick every 100 ms (countPrimes is the function from
section 9):
Future<int> countPrimesAsync(int limit) async {
return countPrimes(limit);
}
Future<void> heartbeat(Stopwatch clock) async {
for (var i = 1; i <= 5; i++) {
await Future.delayed(Duration(milliseconds: 100));
print('tick $i at ${clock.elapsedMilliseconds} ms');
}
}
Future<void> main() async {
final clock = Stopwatch()..start();
heartbeat(clock);
await Future.delayed(Duration(milliseconds: 250));
print('Heavy work starts at ${clock.elapsedMilliseconds} ms');
final primes = await countPrimesAsync(5000000);
print('Heavy work ends at ${clock.elapsedMilliseconds} ms ($primes primes)');
}One run printed (approximate):
tick 1 at 104 ms
tick 2 at 207 ms
Heavy work starts at 253 ms
Heavy work ends at 1065 ms (348513 primes)
tick 3 at 1065 ms
tick 4 at 1167 ms
tick 5 at 1270 msTicks 3 to 5 should have printed at about 300, 400, and 500 ms. The heartbeat simply stopped while the primes were counted.
11. Isolates: Dart's Answer
The fix is a second cook. An isolate is a separate worker with its own memory and its own event loop, running on another thread — real parallelism. The heavy calculation runs there while the main isolate keeps ticking, and the result comes back as a message. Isolates share no variables, so the problems of two threads changing the same value at once cannot happen. We will create isolates in Lecture 098 — Isolates.
12. A Real-World Example
Sara places an order. The app shows the confirmation at once, updates the cart total as a microtask, and books delivery as an event:
import 'dart:async';
void main() {
final prices = [500, 250, 120];
print('Order placed by Sara');
Timer.run(() {
print('Delivery booked');
scheduleMicrotask(() => print('Delivery saved to history'));
});
Future(() => print('Receipt emailed'));
scheduleMicrotask(() {
final total = prices.reduce((a, b) => a + b);
print('Cart total updated: $total');
});
print('Showing confirmation screen');
}Order placed by Sara
Showing confirmation screen
Cart total updated: 870
Delivery booked
Delivery saved to history
Receipt emailedSynchronous lines first, then the microtask, then the first event — whose own microtask runs before the second event.
Code Examples
Example 1 — Microtask Beats Event
import 'dart:async';
void main() {
Future(() => print('Event'));
scheduleMicrotask(() => print('Microtask'));
print('Sync');
}Sync
Microtask
EventExample 2 — Future.microtask vs Timer.run
import 'dart:async';
void main() {
Timer.run(() => print('Timer.run'));
Future.microtask(() => print('Future.microtask'));
}Future.microtask
Timer.runExample 3 — First In, First Out
import 'dart:async';
void main() {
scheduleMicrotask(() => print('1'));
scheduleMicrotask(() => print('2'));
scheduleMicrotask(() => print('3'));
}1
2
3Example 4 — then on a Completed Future
void main() {
Future(() => print('Future()'));
Future.value('ready').then(print);
}ready
Future()Example 5 — Two Tasks Taking Turns
Future<void> worker(String name) async {
for (var job = 1; job <= 2; job++) {
print('$name does job $job');
await Future.delayed(Duration.zero);
}
}
void main() {
worker('Zayan');
worker('Sara');
}Zayan does job 1
Sara does job 1
Zayan does job 2
Sara does job 2Example 6 — A Microtask From Inside an Event
import 'dart:async';
void main() {
Future(() {
print('A');
scheduleMicrotask(() => print('B'));
});
Future(() => print('C'));
}A
B
CExample 7 — Student Marks
import 'dart:async';
void main() {
final marks = [80, 75, 85];
print('Marks saved for Ali');
Future(() => print('Report emailed'));
scheduleMicrotask(() {
final total = marks.reduce((a, b) => a + b);
print('Total updated: $total');
});
print('Showing marks screen');
}Marks saved for Ali
Showing marks screen
Total updated: 240
Report emailedExample 8 — A Sales Report That Freezes the Spinner
void main() {
final clock = Stopwatch()..start();
Future.delayed(Duration(milliseconds: 50), () {
print('Spinner update at ${clock.elapsedMilliseconds} ms');
});
var total = 0;
for (var i = 0; i < 300000000; i++) {
total += i % 10;
}
print('Total $total at ${clock.elapsedMilliseconds} ms');
}One run printed (approximate):
Total 1350000000 at 339 ms
Spinner update at 341 msHow it works
- The spinner update was due at 50 ms.
- The loop adds 300 million small prices without ever giving the thread back.
- Only when
mainends can the event loop run the overdue spinner update.
Example 9 — Bank Balance With Two Deposits
var balance = 1000;
Future<void> deposit(String who, int amount) async {
await Future.delayed(Duration(milliseconds: 100));
balance += amount;
print('$who deposited $amount, balance $balance');
}
Future<void> main() async {
await Future.wait([deposit('Ali', 200), deposit('Sara', 300)]);
print('Final balance: $balance');
}Ali deposited 200, balance 1200
Sara deposited 300, balance 1500
Final balance: 1500How it works
- Both deposits are in progress at the same time — concurrency.
- Each
balance += amountline runs to the end before anything else can run, because there is only one thread. - No deposit is lost, and the final balance is exactly 1500.
Example 10 — Temperature Readings
import 'dart:async';
void main() {
final readings = [21.5, 22.0, 22.8];
for (final reading in readings) {
Timer.run(() {
print('Reading: $reading °C');
scheduleMicrotask(() => print(' saved $reading'));
});
}
print('Sensor started');
}Sensor started
Reading: 21.5 °C
saved 21.5
Reading: 22.0 °C
saved 22.0
Reading: 22.8 °C
saved 22.8How it works
- The loop puts three events in the queue;
Sensor startedprints first. - After each event, its microtask runs before the next event starts.
Common Beginner Mistakes
1. Forgetting the dart:async import
void main() {
// scheduleMicrotask(() => print('Hi')); // ✗
// Timer.run(() => print('Hi')); // ✗
}Error: Method not found: 'scheduleMicrotask'.
Error: Undefined name 'Timer'.Add import 'dart:async'; at the top. Future needs no import; these two do.
2. Thinking Future() runs "as soon as possible"
Future(() => ...) goes to the event queue, behind every microtask and every event
already waiting. Use Future.microtask when the job must run first.
3. Thinking async means "on another thread"
An async function runs on the same thread. A heavy calculation inside it still freezes
everything (section 10).
4. Endless microtasks
A microtask that always schedules another one starves timers, input, and I/O forever (section 8).
5. Relying on exact milliseconds
Timings vary between runs and machines. Reason about order, not about precise numbers.
Best Practices
- Use plain
Future,async, andawaitin everyday code; reach forscheduleMicrotaskonly when something truly must run before the next event. - Keep every microtask short, and never let microtasks schedule each other without a limit.
- Keep synchronous chunks of work small so the event loop stays responsive.
- Measure suspected slow code with
Stopwatchbefore deciding it needs an isolate. - Move heavy CPU work off the main isolate (Lecture 098).
Mini Challenge
Using exactly one print, one scheduleMicrotask, one Future(), and one Timer.run —
and no delays — make Zayan's shop print:
Order placed
Payment checked
Receipt printed
Delivery bookedTry it yourself before opening the solution.
Show solution
import 'dart:async';
void main() {
Future(() => print('Receipt printed'));
Timer.run(() => print('Delivery booked'));
scheduleMicrotask(() => print('Payment checked'));
print('Order placed');
}Order placed
Payment checked
Receipt printed
Delivery bookedThe microtask beats both events, and Timer.run and Future() are both events, so the one
scheduled first runs first: Future() must come before Timer.run.
Practice Questions
Work through these in order — they run from beginner to application to challenge.
Beginner
- Write a program that schedules a microtask printing
Hello from a microtask. - Predict the output:
print('A');, thenFuture(() => print('B'));, thenscheduleMicrotask(() => print('C'));. - In one sentence each, explain concurrency and parallelism.
Application
-
Predict the output, then run it:
import 'dart:async'; void main() { Timer.run(() => print('X')); Future.microtask(() => print('Y')); Future.value(0).then((_) => print('Z')); print('W'); } -
Predict the output, then run it:
import 'dart:async'; void main() { Future(() { print('1'); Future(() => print('4')); scheduleMicrotask(() => print('2')); }); Future(() => print('3')); } -
Write two async functions for Ali and Sara that each print two steps, with
await Future.delayed(Duration.zero)between, so the output alternates. -
Change only the second line so that
Cache updatedprints beforeScreen redrawn:import 'dart:async'; void main() { Timer.run(() => print('Screen redrawn')); Future(() => print('Cache updated')); } -
Use
Stopwatchto timecountPrimes(1000000)(section 9) and print the count and time.
Challenge
-
Schedule a reminder with
Future.delayedfor 100 ms, then callcountPrimes(3000000). Print when the reminder actually ran, and explain the number. -
Predict the output, then run it:
import 'dart:async'; void main() { print('start'); Future(() { print('event A'); Future.microtask(() => print('microtask from A')); }); scheduleMicrotask(() { print('microtask M'); Timer.run(() => print('timer from M')); }); print('end'); }
Practice Answers
Question 1 — Solution
import 'dart:async';
void main() {
scheduleMicrotask(() => print('Hello from a microtask'));
}Hello from a microtaskQuestion 2 — Solution
A
C
BA is synchronous; the microtask C runs before the event B.
Question 3 — Solution
Concurrency: several tasks are in progress at the same time, taking turns. Parallelism: several tasks run at the same instant on different threads.
Question 4 — Solution
W
Y
Z
XW is synchronous. Y and Z are microtasks in scheduling order. X is an event.
Question 5 — Solution
1
2
3
42 is a microtask created during event 1. 3 was queued before 4.
Question 6 — Solution
Future<void> ali() async {
print('Ali: step 1');
await Future.delayed(Duration.zero);
print('Ali: step 2');
}
Future<void> sara() async {
print('Sara: step 1');
await Future.delayed(Duration.zero);
print('Sara: step 2');
}
void main() {
ali();
sara();
}Ali: step 1
Sara: step 1
Ali: step 2
Sara: step 2Question 7 — Solution
Replace Future( with Future.microtask(, so the job goes to the microtask queue:
import 'dart:async';
void main() {
Timer.run(() => print('Screen redrawn'));
Future.microtask(() => print('Cache updated'));
}Cache updated
Screen redrawnQuestion 8 — Solution
int countPrimes(int limit) {
var count = 0;
for (var n = 2; n <= limit; n++) {
var isPrime = true;
for (var d = 2; d * d <= n; d++) {
if (n % d == 0) {
isPrime = false;
break;
}
}
if (isPrime) count++;
}
return count;
}
void main() {
final clock = Stopwatch()..start();
final primes = countPrimes(1000000);
print('$primes primes in ${clock.elapsedMilliseconds} ms');
}One run printed (approximate):
78498 primes in 87 msQuestion 9 — Solution
Using countPrimes from Question 8:
void main() {
final clock = Stopwatch()..start();
Future.delayed(Duration(milliseconds: 100), () {
print('Reminder ran at ${clock.elapsedMilliseconds} ms');
});
print('${countPrimes(3000000)} primes');
}One run printed (approximate):
216816 primes
Reminder ran at 402 msThe reminder was ready at 100 ms, but the event loop takes a new event only after the
running code — the whole calculation and the rest of main — has finished.
Question 10 — Solution
start
end
microtask M
event A
microtask from A
timer from Mmicrotask M runs before any event. It adds timer from M to the end of the event
queue, behind event A. event A creates a microtask, which runs before the next event.
Quick Revision
- What is the difference between concurrency and parallelism?
- How many threads run your Dart code in one isolate?
- Name the two queues and say which one runs first.
- Which two calls add to the microtask queue? Which three add to the event queue?
- Why does
.thenonFuture.valuerun beforeFuture()? - What happens to timers if microtasks keep scheduling more microtasks?
- Why does marking a heavy function
asyncnot stop it from freezing the program?
Key Takeaways
- Concurrency is tasks taking turns; parallelism is tasks running at the same instant.
- Dart's async code is concurrent on one thread;
awaithands the thread back to the event loop. - The loop: run
main, empty the microtask queue, run one event, repeat. scheduleMicrotask,Future.microtask, and.thenon a completed future go to the microtask queue.Future(),Future.delayed, andTimer.rungo to the event queue; each queue is first in, first out.- Running code is never interrupted; endless microtasks starve every event.
- Heavy CPU work freezes timers and async code,
asyncor not. - Isolates add real parallelism with separate memory.
What's Next?
Lecture 098 — Isolates. We proved that counting primes stops every timer and heartbeat in the program. Next we will start a second isolate, run that calculation on it, and keep the main isolate responsive while the work happens in parallel.