Chapters

Hide chapters

Dart Apprentice: Beyond the Basics

First Edition · Flutter · Dart 2.18 · VS Code 1.71

Dart Apprentice: Beyond the Basics

Section 1: 15 chapters
Show chapters Hide chapters

11. Concurrency
Written by Jonathan Sande

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Your computer does a lot of work and does it so fast that you don’t usually realize how much it’s doing. Now and then, though — especially on an older computer or phone — you might notice an app slow down or even freeze. This might express itself during an animation as jank: that annoying stutter that happens when the device does so much work that some animation frames get dropped.

Long-running tasks generally fall into two categories: I/O tasks and computationally intensive tasks. I/O, or input-output, includes reading and writing files, accessing a database or downloading content from the internet. These all happen outside the CPU, so the CPU has to wait for them to complete. On the other hand, computationally intensive tasks happen inside the CPU. These tasks might include decrypting data, performing a mathematical calculation or parsing JSON.

As a developer, you must consider how your app, and particularly your UI, will respond when it meets these time-consuming tasks. Can you imagine if a user clicked a download button in your app, and the app froze until the 20 MB download was complete? You’d be collecting one-star reviews in a hurry.

Thankfully, Dart has a powerful solution baked into the very core of the language, allowing you to handle delays gracefully without blocking your app’s responsiveness.

Concurrency in Dart

A thread is a sequence of commands that a computer executes. Some programming languages support multithreading — running multiple threads simultaneously — but others don’t. Dart is a single-threaded language.

“What? Was it designed back in 1990 or something?”

No, Dart was created in 2011, well into the age of multicore CPUs.

“What a waste of all those other processing cores!”

Ah, but no. The developers deliberately made Dart single-threaded, providing significant advantages, as you’ll soon see.

Parallelism vs. Concurrency

To understand Dart’s model for handling long-running tasks and to see why Dart’s creators decided to make Dart single-threaded, it helps to understand the difference between parallelism and concurrency. In common English, these words mean about the same thing, but a distinction exists in computer science.

A Problem With Parallelism

Little Susie has four pieces of chocolate left in the box next to her bed. She used to have ten, but she’s already eaten six of them. She’s saved the best ones for last because three friends are coming home with her after school today. She can’t wait to share the chocolates with them. Imagine her horror, though, when she gets home and finds only two pieces of chocolate left in the box! After a lengthy investigation, it turns out that Susie’s brother had discovered the stash and helped himself to two of the chocolates. From then on, Susie locked the box whenever she left home.

Dart Isolates

Dart’s single thread runs in what it calls an isolate. Each isolate has its own allocated memory, ensuring that no isolate can access any other isolate’s state. That means there’s no need for a complicated locking system. It also means sensitive data is much more secure. Such a system greatly reduces the cognitive load on a programmer.

Vgufotyafr cuwvt ih genolzeb

Lyicohrucx vesks vocbefcomxgc

Synchronous vs. Asynchronous Code

The word synchronous consists of syn, meaning “together”, and chron, meaning “time”, thus together in time. Synchronous code executes each instruction in order, one line of code immediately following the previous one.

print('first');
print('second');
print('third');
first
second
third

The Event Loop

You’ve learned that Dart employs concurrency on a single thread, but how does Dart manage to schedule tasks asynchronously? Dart uses what it calls an event loop to execute tasks that had been postponed.

Xljrhmeroaj zimu Ejfgpdmetooj yoqa 8 3 1 3 8 8 8 4 Ixiqq kooio 5 4 5 Musdoquff xeiaa Coey akulugi 0 Axajx Wouy 2 0

Running Code in Parallel

When people say Dart is single-threaded, they mean Dart only runs on a single thread in the isolate. However, that doesn’t mean you can’t have tasks running on another thread. One example of this is when the underlying platform performs some work at the request of Dart. For example, when you ask to read a file on the system, that work isn’t happening on the Dart thread. The system is doing the work inside its own process. Once the system finishes its work, it passes the result back to Dart, and Dart schedules some code to handle the result in the event queue. A lot of the I/O work from the dart:io library happens this way.

Observing the Event Loop

Theory is nice, but it’s time for some cold, hard code. In this chapter, you’ll use the Future class to observe the event loop by adding tasks to the event and microtask queues. In Chapter 12, “Futures”, you’ll learn to use Future for more practical applications.

Adding a Task to the Event Queue

Passing a block of code to Future causes Dart to put that code on the event queue rather than running it synchronously.

print('first');

Future(
  () => print('second'),
);

print('third');
first
third
second

Adding a Task to the Microtask Queue

You’ll only need to add a task to the microtask queue once in a blue moon.

print('first');

Future(
  () => print('second'),
);

Future.microtask(
  () => print('third'),
);

print('fourth');
first
fourth
third
second

Running Synchronous Code After an Event Queue Task

Sometimes you might want to perform a task immediately after a task from the event queue finishes.

print('first');

Future(
  () => print('second'),
).then(
  (value) => print('third'),
);

Future(
  () => print('fourth'),
);

print('fifth');
first
fifth
second
third
fourth

Intentionally Delaying a Task

Sometimes, it’s useful to simulate a long-running task. You can accomplish this with Future.delayed. Dart will add a task to the event queue after some time.

print('first');

Future.delayed(
  Duration(seconds: 2),
  () => print('second'),
);

print('third');
first
third
first
third
second

Challenge

Before moving on, here’s a challenge to test your understanding of how Dart handles asynchronous tasks. An explanation follows the challenge, but try to figure out the solution yourself before looking.

Challenge 1: What Order?

In what order will Dart print the numbered statements? Why?

void main() {
  print('1 synchronous');
  Future(() => print('2 event queue')).then(
    (value) => print('3 synchronous'),
  );
  Future.microtask(() => print('4 microtask queue'));
  Future.microtask(() => print('5 microtask queue'));
  Future.delayed(
    Duration(seconds: 1),
    () => print('6 event queue'),
  );
  Future(() => print('7 event queue')).then(
    (value) => Future(() => print('8 event queue')),
  );
  Future(() => print('9 event queue')).then(
    (value) => Future.microtask(
      () => print('10 microtask queue'),
    ),
  );
  print('11 synchronous');
}

Solution to Challenge 1

For brevity, the explanations below will refer to each task by its number. For example, print('1 synchronous') is abbreviated as 1.

Step 0

void main() {
  // ...
}

Dart creates the main isolate and calls your main function:

Uwepv douua Methuqigd noaii wiiz() Jooj idofexe

Step 1

print('1 synchronous');

1 is synchronous, so Dart executes it immediately in the main isolate:

Ewibv laiie Lovwehims keaee 0 Cuuv iyofoye

Step 2

Future(() => print('2 event queue')).then(
  (value) => print('3 synchronous'),
);

Dart adds 2 to the event queue.

Ejozp caouu Romruxokt laeae Tuix ecudayi 2

Step 3

Future.microtask(() => print('4 microtask queue'));

Dart adds 4 to the microtask queue:

Iwaym xiaie Xuwwukutb peeoa Koub imogita 6 1

Step 4

Future.microtask(() => print('5 microtask queue'));

Dart adds 5 to the microtask queue:

Ukuhw neooa Vixvuhers joueo Moux ohoyaza 6 4 0

Step 5

Future.delayed(
  Duration(seconds: 1),
  () => print('6 event queue'),
);

Dart starts an internal timer for one second. The queues remain unchanged:

Ugihl luuuo Nasquwazq zaiie Ruuw ipiceri 4 5 1

Step 6

Future(() => print('7 event queue')).then(
  (value) => Future(() => print('8 event queue')),
);

Dart adds 7 to the event queue:

0 Omeqp keoao Wazxucoyj foeeo Rois ogepixe 0 6 0

Step 7

Future(() => print('9 event queue')).then(
  (value) => Future.microtask(
    () => print('10 microtask queue'),
  ),
);

Dart adds 9 to the event queue:

9 Awemv deeiu Jutfoleqn haiui Mout inehiwo 4 7 4 8

Step 8

print('11 synchronous');

11 is synchronous, so Dart executes it immediately:

4 Ipogl kiaao Galzoqipb roiia 50 Goar ovaxili 9 4 4 3

Step 9

print('4 microtask queue');

All the synchronous tasks have finished, so Dart executes the first task in the microtask queue:

6 7 1 Ikatv gaouo 3 Zekbaguty paauu 3 Taap azonasi

Step 10

print('5 microtask queue');

Dart then executes the next task in the microtask queue:

5 8 3 Ohabh saaoa Racyivebv zeeii 1 Waom akunifa

Step 11

print('2 event queue');

The microtask queue is empty now, so Dart takes the first task off of the event queue and executes it in the main isolate:

8 0 Oqomj vaiia Bimguvetw pooai 0 Wior otefuhi

Step 12

Future(() => print('2 event queue')).then(
  (value) => print('3 synchronous'),
);

As soon as 2 finishes, Dart executes 3 synchronously:

8 Apijq nieai Serdunofh xeiae 9 Poeq iyipire 6

Step 13

print('7 event queue');

Dart takes 7 off of the event queue and executes it:

7 Isagr waaii Citxevert doaou 8 Yiuq eyorare

Step 14

Future(() => print('7 event queue')).then(
  (value) => Future(() => print('8 event queue')),
);

When 7 finishes, Dart schedules 8 at the end of the event queue:

7 Onatc jaiue Vuslegefd zieoi Wuiz aguqixo 3

Step 15

print('9 event queue');

Dart takes 9 off of the event queue and executes it:

0 Ugefv deeii Qaqlehidz rooau 4 Ruiv esonude

Step 16

Future(() => print('9 event queue')).then(
  (value) => Future.microtask(
    () => print('10 microtask queue'),
  ),
);

When 9 finishes, Dart adds 10 to the microtask queue:

Okimh nuiiu Xarlusahr teuee Waaj analuxe 6 86

Step 17

print('10 microtask queue');

The microtask queue has priority over the event queue, so Dart executes 10 before 8:

2 Aderc muoao Pejbiwolm biiuo 32 Xeum ugezeyo

Step 18

print('8 event queue');

The microtask queue is empty now, so Dart takes 8 off of the event queue and executes it:

Azuwv wieao Huywodasl biaei 1 Juer ebuzuga

Step 19

The queues are all empty now:

Esehj viuoa Papnunowl koiuu Dieb ifibaxu

Step 20

Future.delayed(
  Duration(seconds: 1),
  () => print('6 event queue'),
);

Sometime later, the duration finally completes, so Dart adds 6 to the event queue:

Eyosg suauo Biqhuguzy juioe Wiep ecukoqo 2

Step 21

print('6 event queue');

There’s nothing to wait for, so Dart takes 6 off the event queue and executes it:

Ozisx hiioa Gegqibokd lueou 1 Seeg ufebova

Step 22

The queues are all empty again:

Ecobq qiiuo Silrowezl qaioo Noej alelusi

Result

Here is the final output:

1 synchronous
11 synchronous
4 microtask queue
5 microtask queue
2 event queue
3 synchronous
7 event queue
9 event queue
10 microtask queue
8 event queue
6 event queue

Key Points

  • Dart is single-threaded and handles asynchronous programming through concurrency rather than parallelism.
  • Concurrency refers to rescheduling tasks to run later on the same thread, whereas parallelism refers to running tasks simultaneously on different threads.
  • Dart uses an event loop to schedule asynchronous tasks
  • The event loop has an event queue and a microtask queue.
  • A queue is a first-in-first-out (FIFO) data structure.
  • Synchronous code always runs first and cannot be interrupted. After this comes anything in the microtask queue, and when these finish, any tasks in the event queue.
  • You can run code in parallel by creating a new isolate.

Where to Go From Here?

You learned about queues as first-in-first-out data structures in this chapter. If you’d like to learn more, as well as how to build a queue, check out the “Queues” chapter in Data Structures & Algorithms in Dart.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now