Dart Concurrency and Isolate

Concurrency is the ability to handle multiple tasks simultaneously.

Dart's concurrency model differs from traditional multithreading; it uses Isolates to achieve parallel computation.

This chapter introduces Dart's concurrency model, the concept and usage of Isolates, and the message passing mechanism.


Dart Concurrency Model

Most programming languages use a shared-memory multithreading model, where multiple threads share the same memory space.

The drawback of this model is that it is prone to race conditions and deadlocks.

Dart adopts a different strategy:Each Isolate has its own independent memory heapIsolates do not share memory.

They communicate via message passing, which fundamentally avoids data race issues.

Dart's concurrency model has three levels:

LevelMechanismApplicable scenarios
Event loopSingle-threaded asynchronous (Event Loop)I/O operations, timers, and user interaction.
IsolateIsolated memory + message passingCPU-intensive computation
Future/StreamAsynchronous programming syntaxMost everyday development scenarios

The concurrency needs of most Dart programs can be satisfied with async/await and Future/Stream. Isolates are only necessary when you need to perform heavy CPU-bound computation. Introducing Isolates prematurely makes code more complex with little benefit.


Isolate concept and usage

An Isolate is Dart's unit of concurrency, and each Isolate has its own independent memory and event loop.

The main program itself runs in an Isolate (the main Isolate).

Use Isolate.spawn to create a new Isolate

Example

import 'dart:isolate';

// This function will run in the new Isolate
// SendPort is used to send messages to the main Isolate
void heavyComputation(SendPort sendPort) {
  print('New Isolate starts computing...');

  // Simulate CPU-intensive computation
  int sum = 0;
  for (int i = 1; i <= 10000000; i++) {
    sum += i;
  }

  // Send the computation result back to the main Isolate
  sendPort.send(sum);

  print('New Isolate computation complete, result sent');
}

Future<void> main() async {
  print('Main Isolate started');

  // Create a ReceivePort to receive messages
  var receivePort = ReceivePort();

  // spawn creates a new Isolate
  await Isolate.spawn(heavyComputation, receivePort.sendPort);

  print('Main Isolate can do other things while waiting for the result...');

  // Wait for the computation result from the new Isolate
  var result = await receivePort.first;
  print('EXAMPLE calculation result: sum of 1 to 10000000 = $result');

  receivePort.close();
  print('Main Isolate ended');
}
主 Isolate 启动
主 Isolate 在等待结果的同时可以做其他事情...
新 Isolate 开始计算...
新 Isolate 计算完成,结果已发送
EXAMPLE 计算结果: 1 到 10000000 的和 = 50000005000000
主 Isolate 结束

Comparison: Performance differences with and without Isolates.

Example

// Execute a CPU-intensive task in the main Isolate (will block)
int fibonacci(int n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

void main() {
  // Compute in the main Isolate — will block all other operations
  var startTime = DateTime.now();

  // Note: if n is too large, this calculation will be very time-consuming.
  // In real development, this kind of calculation should be placed in a separate Isolate.
  var result = fibonacci(40);

  var elapsed = DateTime.now().difference(startTime);
  print('EXAMPLE Fibonacci(40) = $result');
  print('Elapsed time: ${elapsed.inMilliseconds}ms');
  print('(Note: during the calculation, the main Isolate cannot handle other tasks)');
}

In Flutter applications, if a time-consuming synchronous calculation is executed in the main Isolate, it will cause the UI to freeze. The solution is to put the computation task in a separate Isolate, and after the computation is complete, send the result back via messages and update the UI.


Message passing mechanism

Isolates communicate by passing messages through SendPort and ReceivePort.

Messages must be serializable (primitive types, String, List, Map, etc.); functions or closures cannot be passed.

Two-way communication

Example

import 'dart:isolate';

// The worker function that runs in the new Isolate
void workerIsolate(SendPort mainSendPort) {
  // Create its own ReceivePort to receive messages from the main Isolate
  var workerReceivePort = ReceivePort();

  // First send the worker's SendPort to the main Isolate to establish a two-way channel
  mainSendPort.send(workerReceivePort.sendPort);

  print('Worker Isolate: waiting for tasks...');

  // Listen for tasks sent by the main Isolate
  workerReceivePort.listen((message) {
    if (message is List<int>) {
      // Received task: square each number in the list
      print('Worker Isolate: received data $message');
      var result = message.map((n) => n * n).toList();

      // Send the result back via mainSendPort
      mainSendPort.send(result);
    } else if (message == 'exit') {
      print('Worker Isolate: received exit signal, shutting down');
      workerReceivePort.close();
      mainSendPort.send('goodbye');
    }
  });
}

Future<void> main() async {
  print('Main Isolate: starting');

  // Create main receive port
  var mainReceivePort = ReceivePort();

  // Start Worker Isolate
  await Isolate.spawn(workerIsolate, mainReceivePort.sendPort);

  // Wait for Worker to send its SendPort (establish two-way communication)
  SendPort? workerSendPort;
  await for (var msg in mainReceivePort) {
    if (msg is SendPort) {
      workerSendPort = msg;
      print('Main Isolate: two-way communication established with Worker');
      break;
    }
  }

  // Send task through Worker's SendPort
  workerSendPort!.send([1, 2, 3, 4, 5]);
  workerSendPort.send([10, 20, 30]);

  // Receive Worker's calculation result
  int responseCount = 0;
  await for (var msg in mainReceivePort) {
    if (msg is List<int>) {
      print('Main Isolate: received result $msg');
      responseCount++;
      if (responseCount == 2) break;
    }
  }

  // Send exit signal
  workerSendPort.send('exit');
  await for (var msg in mainReceivePort) {
    if (msg == 'goodbye') {
      print('Main Isolate: Worker has exited');
      break;
    }
  }

  mainReceivePort.close();
  print('EXAMPLE two-way communication demo ended');
}
主 Isolate: 启动
主 Isolate: 已建立与 Worker 的双向通信
Worker Isolate: 等待任务...
Worker Isolate: 收到数据 [1, 2, 3, 4, 5]
主 Isolate: 收到结果 [1, 4, 9, 16, 25]
Worker Isolate: 收到数据 [10, 20, 30]
主 Isolate: 收到结果 [100, 400, 900]
Worker Isolate: 收到退出信号,关闭
主 Isolate: Worker 已退出
EXAMPLE 双向通信演示结束

Simplify using Isolate.run (Dart 3.0+)

Dart 3.0 introduced Isolate.run(), greatly simplifying the use of one-off computational tasks.

Example

import 'dart:isolate';

// A time-consuming calculation function
int complexCalculation(int n) {
  int result = 0;
  for (int i = 0; i < n; i++) {
    result += i * i;
  }
  return result;
}

Future<void> main() async {
  print('Starting calculation...');

  // Isolate.run: one line of code, automatically creates an Isolate, executes, and returns the result
  var result = await Isolate.run(() => complexCalculation(10000000));
  print('EXAMPLE calculation result: $result');

  print('Calculation complete');

  // Comparison: if not using Isolate
  print('(If calculated directly in the main Isolate, it will block other operations)');
}
开始计算...
EXAMPLE 计算结果: 333333283333335000000
计算完成
(如果在主 Isolate 中直接计算,会阻塞其他操作)

Isolate.run() is suitable for scenarios where you want to "execute a calculation once and return the result". If you need continuous, two-way communication (such as a long-running background service), you need to use Isolate.spawn() + SendPort/ReceivePort.

Limitations of Isolate message passing.

Can be passedCannot be passed
null、bool、int、double、StringFunctions, closures
List, Map, Set (elements can also be passed)Stream、Future
SendPort (used to establish a communication chain).Most objects of non-primitive types
Capability (permission token)File handles, network sockets
TransferableTypedData (efficiently transfers large chunks of data)Custom classes (unless serializable)

Concurrency model comparison

FeaturesDart IsolateTraditional multithreading (e.g., Java)
Memory modelIndependent memory, no sharingShared memory
Communication methodMessage passing (SendPort)Shared variables + locks
Data raceDoes not exist (no sharing)Requires lock mechanism protection
Deadlock riskAlmost nonexistentExists
Creation overheadRelatively largeRelatively small
Applicable scenariosCPU-intensive parallel computingGeneral concurrency

Although Dart's Isolate model avoids data races, the tradeoff is the relatively high overhead of creation and communication. Do not create thousands of Isolates—usually a few Isolates are enough. For I/O-bound tasks, using async/await is the best choice.

other extensions