Flutter/Dart non-blocking demystify

TruongSinh Tran-Nguyen
Jun 11 · 5 min read

Update: Coincidentally, Flutter team just released a clip. It illustrates non-blocking concepts visually beautiful. In the article below, it is claimed that Dart is NOT a single-threaded, while this video claims otherwise. I will leave the article as is while researching and discussing more.

TLDR: Flutter/Dart is NOT single-threaded; Dart’s concurrency model is NOT Java’s thread; Future/Async/Await runs on the same thread and solves IO-bound problems, while Dart’s Isolate/Flutter’s compute runs on a different isolated (no-shared-memory) thread and solves CPU-bound ones.

Myth #1: Flutter/Dart is single-threaded

All over the internet, I see people talking Dart is a single-threaded language, and thus so is Flutter. I would argue that saying Dart is a single-threaded language is not technically true. Let me break it down.

It is true that any piece of Dart code is executed in a single thread. That is to say, any piece of code, having no callback and await keyword, is guaranteed to be executed uninterrupted.

However, in my opinion, a single-threaded language is an old kind of programming language that cannot take advantage of multi-core processors. For the sake of simplicity, let us say single-threaded language is the opposite of concurrent language (although concurrency is not parallelism). In other words, on multi-core processors, applications written properly in a concurrent language can do several computations in parallel and make use of multi-core, while those written in single-threaded language perform no better than those running on single-core.

Dart actually supports multi-threading (kind of, see next myth) using Isolates, and with this, Flutter can take full advantage of modern multi-core processor on mobile or desktop.

Myth #2: Flutter/Dart concurrency model is the same as Java’s thread

As explained in Myth #1, Dart supports multi-threading using Isolates. Right in the introduction to Isolates, it has been said that

isolates [are] independent workers that are similar to threads but don’t share memory, communicating only via messages.

Basically, it is similar to the latest browser/JavaScript’s Web Workers API or NodeJS’ Worker Threads. A side note, by the same definition in Myth #1, JavaScript has been a single-threaded language until it has Web Workers or Worker Threads API (Node’s Cluster API is about fork/process, not thread).

Some people, when confronted with a problem, think, “I know, I’ll use threads,” and then two they hav erpoblesms.

Because Dart is using message passing pattern and there is no shared memory/variables, there is hardly any need for lock or mutex. It is also noted that

You may experience errors if you try to pass more complex objects, such as a Future or http.Response between isolates.

In simpler words, most of the time, communication among Dart’s isolates must be serializable.

Myth #3: Future/Async/Await runs code in separate threads and solves all blocking problems

Totally not true. Future/Async/Await runs the code in the same thread. If there is a need to wait for external data/events, Dart’s event queue shall be used, but still in the same thread. Thus, Future/Async/Await only solves half of the blocking problems.

To have a better understanding, we should take a step back. Blocking in programming can be grouped into 2 categories, I/O-bound and CPU-bound.

I/O refers to the data in motion, for example, network HTTP request/response, reading/writing data to local storage, message passing among threads/isolates (see Myth #2). The waiting, for example, when we send out an HTTP request and wait for the HTTP response, can be several seconds, and in that several seconds, our app can be blocked and UI can be frozen if not written properly.

In Java/Kotlin or ObjC/Swift, by default, most of IO-bound operations are blocking, and developers must consciously do some extra code to have it “off” the “main thread” with various techniques. In Dart, by default, all IO-bound operations are async and return Future. Developers can opt-in to use blocking variations by appending Sync prefix, such as File’s methods copy vs copySync, lastAccessed vs lastAccessedSync.

However, Future/Async/Await does not unblock CPU-bound operations. Take a look at this simple example

In this case, using Future/Async/Await actually makes the performance much much worse, like 180x worse (oh and be careful when tweaking the param, veryLongRunningCpuBoundFunction is designed to have O(nⁿ) time complexity 😈), but does not solve the blocking problem. If you can’t believe, simply copy-paste veryLongRunningCpuBoundFunction (either blocking or async version) and run in your Flutter app, your UI will completely be frozen as soon as the function is invoked.

veryLongRunningCpuBoundFunction may seem unrealistically simple and/or stupid. In practice, common CPU-bound operations are:

  • matrix multiplication
  • cryptography-related (such as signing, hashing, key generation)
  • image/audio/video manipulation
  • serialization/deserialization
  • offline machine learning model computation
  • compression (such as zlib)
  • Regular expression Denial of Service — ReDoS

In such cases, we should use the tool we have been discussing in the previous 2 myths, Isolate. Dart’s Isolate API, in my opinion, is not the easiest to use. Fortunately, Flutter has a utility function, compute which is basically a wrapper around Dart’s Isolate with much simpler (but somewhat limited) API. In fact, the example from Flutter is about parsing JSON in the background (parsing JSON is categorized under serialization/deserialization case of CPU-bound operations). It’s also worth noticing that compute returns a Future, because the CPU-bound code (parsing JSON) is now running in a different isolate, and isolate communication (message passing) is IO-bound.

Revisiting the example above, here’s the correct way to run the code without having UI frozen.

Again, it’s worth noticing that compute returns a Future, but veryLongRunningCpuBoundFunction itself returns a primitive data type (in this case int).

Dart’s Isolate / Flutter’s compute is an extremely important point for people coming to Flutter from iOS/Android background.

Dart’s Isolate / Flutter’s compute is an extremely important point for people coming to Flutter from iOS/Android background. As explained above, iOS/Android traditionally do not distinguish between IO-bound and CPU-bound blocking operations, thus developers always consciously and manually use the same techniques to deal with any blocking operations (threading + Future/Callback/Stream on Android, GCD on iOS). In Dart/Flutter, all IO-bound blocking operations have already been identified and solved by the language with Future, but newcomers should be aware of Dart’s Isolate / Flutter’s compute for computational-heavy operations to avoid UI frozen.

Conclusion

Flutter/Dart is not technically single-threaded, even though Dart code is executed in a single thread. Dart is a concurrent language with message passing pattern, that can take full advantage of modern multi-core architecture, without worrying about lock or mutex. Blocking in Dart can be either I/O-bound or CPU-bound, which should be solved, respectively, by Future and Dart’s Isolate/Flutter’s compute.


Flutter Community

Articles and Stories from the Flutter Community

TruongSinh Tran-Nguyen

Written by

Engineering Director — Inspectorio. 10ys working with startups in Finland and Vietnam. Nordic Startup Award — People’s choice CTO 2016. Agile Evangelist-PSM III

Flutter Community

Articles and Stories from the Flutter Community