I’m writing an API server in all-Dart. I setup multiple isolatesso that my server would scale on the Mac Mini it’s running on (2018 6-core, 12 threads, 3.2Ghz, Intel i7). As I started to investigate Dart isolates to figure out if they would allow my API server to scale, I ran into a lot of articles claiming that Dart was single threaded and that isolates are not threads.
If they’re not threads, but I can spawn an isolate, does that mean it’s a process? When I listed running processes, I couldn’t find multiples running (I was spawning 12 isolates). Given that everything I read emphasized the “Dart is single threaded”, I never took the time to dig into the threads running (pretty silly on my part it turns out).
After asking some questions of some very helpful folks at Google who kindly reset my brain by reminding me that Dart is a VM (at least how I run it), so there is only one Dart VM process, I did also discover that isolates really are threads.
Normally, when we think of threads (as in C/C++ threads or Unix pthreads), we think of execution units that all share the same memory heap and can communicate through shared memory. Dart isolates (as the name implies) are all isolated from one another. This is no shared memory (or shared anything). The only way to communicate between isolates is ports that are setup during isolate spawning/creation. But, isolates are threads (as far as the system is concerned), they are just isolated from one another.
To really be able to see this and prove it to myself, I decided to write a simple Dart application that spawns isolates that just keep the CPU busy and see what happens when I look at
ps -M <pid>. The combination of these two commands would let me see threads running inside the Dart VM process, and also see if all my cores/threads were being kept busy.
Here is the simple Dart app:
main() function spawns as many isolates as
maxIsolates is set to. For my API server, I use
Platform.numberOfProcessors for this value. Which (in my MacMini case) will spawn 12 isolates (one for each hardware thread). The
_createIsolate function does the work to keep the CPU busy. If I run this as is, it takes about 50 seconds to run. But, if I change
maxIsolates to 10, and remove one of the 0 from the for loop, I see my 10 isolates get spawned, and the runtime drops to about 10 seconds (give or take).
To give myself some time, I added a 0 back to the for loop. Then, using
ps -M <pid> I could see the threads running:
In this terminal window, I can see the 10 threads running on my Dart process. Now, I wanted to see that my cores/threads were all being kept busy (I don’t really have anything else running on this system. I’m guessing that the extra thread is background work the Dart VM needs to do (like garbage collection)).
htop gives me this view:
At the top of the listing, you can see all 12 hardware threads running at near full capacity. If you don’t have
htop on your system, use
homebrew to install it. You’ll have to trust me that after my CPU busy isolates terminate, all 12 hardware threads pretty much go to 0% busy.
It is true that each Dart isolate is a single threaded event loop. But, it’s pretty easy to create a multi-threaded (multi-isolate) scalable app that takes full advantage of all your system resources. Having done both the shared resource version of “real” threads, and the port communications version of Dart isolates, I’m quickly becoming a fan of the isolate model. It’s much simpler/easier to get your head wrapped around it, and far less susceptible to side effects like race conditions and other typical thread bugs.
Isolates are threads — just well behaved ones! :).