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:
| Level | Mechanism | Applicable scenarios |
|---|---|---|
| Event loop | Single-threaded asynchronous (Event Loop) | I/O operations, timers, and user interaction. |
| Isolate | Isolated memory + message passing | CPU-intensive computation |
| Future/Stream | Asynchronous programming syntax | Most 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
// 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
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
// 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
// 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 passed | Cannot be passed |
|---|---|
| null、bool、int、double、String | Functions, 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
| Features | Dart Isolate | Traditional multithreading (e.g., Java) |
|---|---|---|
| Memory model | Independent memory, no sharing | Shared memory |
| Communication method | Message passing (SendPort) | Shared variables + locks |
| Data race | Does not exist (no sharing) | Requires lock mechanism protection |
| Deadlock risk | Almost nonexistent | Exists |
| Creation overhead | Relatively large | Relatively small |
| Applicable scenarios | CPU-intensive parallel computing | General concurrency |
other extensionsAlthough 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.