Dart is based on Isolate
background
In other languages, in order to make efficient use of multi-core CPU, multi-thread parallelism is usually used to realize concurrent code execution and ensure the cooperation between multi-threads by sharing data. However, this mode gives rise to many problems, such as resource consumption caused by thread opening and deadlock of data sharing agent.
Whether APP or Web, the CPU is idle most of the time and generally does not require intensive and concurrent processing. Dart, as a front-end development and design language, does not use multithreading in concurrency design. Instead, it uses the single-thread model of Isolate (Isolation) to solve the dependency of concurrent tasks on multithreading.
An Isolate
Each Isolate consists of the following components:
(1) Stack is used to store function call context and call link information.
(2) Heap is used to store objects, and Heap memory reclamation management is similar to Java.
(3) Queues for storing asynchronous callbacks are divided into MicroTaskQueue and EventQueue.
(4) and an EventLoop for handling asynchronous callbacks.
Isolate Running code
Code written in DART falls into two types:
Synchronous code: normally written code.
Asynchronous code: Some functions that return types Future and Stream.
Because THE Isolate is a single-threaded model, when code runs it encounters asynchronous code, it is thrown into a Queue and synchronous code is only run sequentially. Asynchronous tasks are executed sequentially only when the synchronous code is complete.
Let’s write a demo to verify this. Future.delayed is the asynchronous code shown below.
Future printc(){
Future.delayed(new Duration(seconds: 2), () {print("a");// Callback for asynchronous code is executed after all synchronous code is executed
});
Future.delayed(new Duration(seconds: 1), () {print("b");// Callback for asynchronous code is executed after all synchronous code is executed
});
print("c");
var start = DateTime.now();
for(int i=0; i<100000; i++){for(int j=0; j<100000;j++){
i*j+j*i-j*i;
}
}
var end = DateTime.now();
print("Synchronizing time-consuming tasks:${end.second-start.second}SEC. "");
print("d");
}
Copy the code
The execution result is as follows:
C Synchronization task :11 seconds D B ACopy the code
To explain the result, asynchronous tasks that print a and B are inserted into the Queue and their callbacks are not currently executed. Run the synchronization code, so c is printed first, and D is printed after the synchronization task is completed. After the synchronous code execution is complete, EventLoop extracts the asynchronous code execution from the Queue. Although the asynchronous task A comes first, it has a long delay, so b is printed first.
Synchronous waiting: Use the await keyword when calling asynchronous code (such as Future) and the async keyword in method declarations.
If you want to run the asynchronous task before executing the synchronous code, you can use await to run the asynchronous task and block the synchronous code. After the asynchronous task is complete, the synchronous code continues to be executed.
Future printc() async{
await Future.delayed(new Duration(seconds: 2), () {print("a");// Callback for asynchronous code is executed after all synchronous code is executed
});
await Future.delayed(new Duration(seconds: 1), () {print("b");// Callback for asynchronous code is executed after all synchronous code is executed
});
print("c");
var start = DateTime.now();
for(int i=0; i<100000; i++){for(int j=0; j<10000;j++){
i*j+j*i-j*i;
}
}
var end = DateTime.now();
print("Synchronizing time-consuming tasks:${end.difference(start).inSeconds}SEC. "");
print("d");
}
Copy the code
Adding the await modifier to the two asynchronous tasks will execute sequentially as if it were synchronous code.
A B C Synchronization time :1 second DCopy the code
EventLoop
Asynchronous tasks are dropped to the EventQueue and executed by the EventLoop. Asynchronous tasks are constantly fetched from the time queue and run.
The following figure shows how EventLoop takes the task from the queue and executes it.
The MicrotaskQueue in the figure is the MicrotaskQueue with the highest priority. Each loop checks the MicrotaskQueue first. If there are any microtasks, the microtasks will be executed first.
Microtasks: Microtasks are usually used to perform very short asynchronous operations and cannot be too large. There are ways to generate microtasks
/ / way
scheduleMicrotask((){
print("I'm a micromission!");
});
2 / / way
Future.microtask(() => print("I'm another microtask!"));
Copy the code
Events: Events, mainly from Future, including IO, gestures, draws, timers, messages that communicate with Isloate, etc.
Note the special scenarios used by the Future here.
Future.delayed(new Duration(seconds: 2), () {print("a");// Callback for asynchronous code is executed after all synchronous code is executed
});
Copy the code
Future.delayed Asynchronous callbacks are added to the end of the EventQueue only after the delay is over, not immediately, and execution time is not guaranteed.
Future(()=>print("zzz"))
.then((value) => print("xxx"))
.then((value) => print("yyy"))
.then((value) => print("www"));
Copy the code
Future.then complements callbacks to asynchronous events, and then does not add events to the EventQueue, but executes immediately after the previous Future execution completes. The order of execution of multiple THEN internal tasks can be guaranteed.
Future aaa = Future(()=>print("aaa"));
// Aaa is in the finished state
aaa.then((value) => print("bbb"));
// Future(()=>null) generates a completed Future
Future(()=>null).then((value) => print("ccc"));
Copy the code
For a Future that has already been completed, calling then is not executed immediately, but instead adds the callback from then to the Microtask queue.
Case Study:
void dartLoopTest() {
Future x0 = Future(() => null);
Future x = Future(() => print('1'));
Future(() => print('2'));
scheduleMicrotask(() => print('3'));
x.then((value) {
print('4');
Future(() => print('5'));
}).then((value) => print('6'));
print('7');
x0.then((zvalue) {
print('8');
scheduleMicrotask(() {
print('9');
});
}).then((value) => print('10'));
}
Copy the code
The output is:
7
3
8
10
9
1
4
6
2
5
Copy the code
-
Print (‘7’);
-
ScheduleMicrotask (() => print(‘3’));
-
The current MicroTask queue is empty, events are fetched from the Event queue and executed in code order
Future x0 = Future(() => null); Copy the code
There is no output at this point, but X0 is the Future of the completed state, and the first then callback is added to the MicroTask queue.
x0.then((zvalue) { print('8'); scheduleMicrotask(() { print('9'); }); }).then((value) => print('10')); Copy the code
Then run the microtask, calling print(‘8’); , perform scheduleMicrotask (() {print (‘ 9 ‘); }); Add print(‘9′) to the microtask queue; The task. Print (’10’) is called first because the current microtask is not finished.
-
Future x = Future(() => print(‘1’)) After that, x is also in the completed state, and the then callback is added to the microtask queue for execution. Print 4 in the microtask, add an event that prints 5 to the event queue, and finally print 6.
x.then((value) { print('4'); Future(() => print('5')); }).then((value) => print('6')); Copy the code
-
There are also events that print 2 and print 5 in the event queue, which are executed in order to output the final result.
void testThenAwait(){
Future(()=>print("AAA"))
.then((value) async= >await Future(()=>print("BBB")))
.then((value) => print("CCC"));
Future(()=>print("DDD"));
}
Copy the code
When we use await in THEN we block the current THEN call down.
Output result:
AAA
DDD
BBB
CCC
Copy the code
Output analysis:
- First, there are two Futures in this method, which are added to the Event queue in sequence.
- Run the first Future, print AAA,
- The first THEN is executed synchronously, adding a third Future to the Event queue
Future(()=>print("BBB")
Must wait due to synchronous waitFuture(()=>print("BBB")
The command can be continued only after the execution is complete. - Run the first asynchronous Event in the Event queue
Future(()=>print("DDD"));
Print DDD, - Run the remaining asynchronous events in the Event queue
Future(()=>print("BBB")
, print the BBB - Then is executed to print CCC
Create the Isolate
Basic method
The code in Flutter applications runs in root isloate by default. Although single-threaded, it is sufficient to handle all kinds of asynchronous tasks.
When there are computationally intensive time-consuming tasks, a new Isolate needs to be created to perform time-consuming calculations to avoid blocking root isloate. Due to memory isolation between different isolates, communication is implemented through ReceivePort and SendPort.
Use isolate. spawn to create a new Isolate. Look at the function signature
external static Future<Isolate> spawn<T>(
void entryPoint(T message), T message,
{bool paused = false.bool errorsAreFatal = true,
SendPort? onExit,
SendPort? onError,
@Since("2.3") String? debugName});
Copy the code
The external modifier indicates that this method has different implementations on different platforms, somewhat similar to Java’s native method.
Future
indicates that it is an asynchronous method. In order to get the Isolate created in the synchronous code, you need to add synchronization and await the call.
EntryPoint defines the function signature (empty return value, with one and only input parameter) that can accept the computation task, which must be either a top-level function or a static method.
T message is the parameter required to run computing tasks in the new Isolate. The type must be the same as that accepted by entryPoint.
So the two main steps to create a new Isolate are as follows:
-
Use top-level functions or static methods to define computational tasks
// Time calculation part int fibonacci(int n) { return n < 2 ? n : fibonacci(n - 2) + fibonacci(n - 1); } // 1. Compute tasks void task1(int start) { DateTime startTime = DateTime.now(); int result = fibonacci(start); DateTime endTime = DateTime.now(); print("Calculation time:${endTime.difference(startTime)}Results:${result.toString()}"); } void main() { task1(50); } // Output: Calculation time: 0:00:48.608656 Result: 12586269025 Copy the code
The calculations above took 48 seconds and had to be done on the new Isolate
-
To prepare for the entry, call spawn to create a new Isolate.
void main() async { Isolate newIsolate = await Isolate.spawn(task1,10,debugName: "isolateDebug"); print("The end!); } Copy the code
Output result:
The end!Copy the code
The results calculated on the new Isolate are not printed to the current Console. In this case, the current Isolate and SendPort are used to establish a connection between the current and new Isolate.
One-way communication
Define time-consuming tasks that can receive SendPort from the host, and send the results back to the host Isolate through Send after calculation.
/// [hostSendPort] Is used to send results to the host ISOLATE
void task2(SendPort hostSendPort){
DateTime startTime = DateTime.now();
int result = fibonacci(47);
DateTime endTime = DateTime.now();
var state = "Calculation time:${endTime.difference(startTime)}Results:${result .toString()}";
hostSendPort.send(state);
}
Copy the code
In the host Isolate, you define a ReceivePort for receiving results, which is also a Stream, and you can set up a listener to handle the received results.
void main() async {
// Define the ReceivePort from which the host receives the result, and set up the listener.
ReceivePort hostReceivePort = ReceivePort();
hostReceivePort.listen((message) {
print(message);
});
// Define hostSendPort that sends data to hostReceivePort
SendPort hostSendPort = hostReceivePort.sendPort;
// hostSendPort.send("message"); We tested the situation where results were sent on the current ISOLATE.
Isolate newIsolate =
await Isolate.spawn(task2, hostSendPort);
}
Copy the code
Calculation results:
Calculation time: 0:00:11.959955 Result: 2971215073Copy the code
Two-way communication
After the above modification, SendPort from the host is sent to the sub-ISOLATE, and the result is calculated and sent back to the host Isolate. The host Isolate processes the result through ReceivePort. Then the child Isolate sends data to the host Isolate.
But how does the host Isolate send data to the child Isolate?
The subisolate sends data to the host Isolate by holding SendPort in the host. In order for the host Isolate to send data to the sub-isolate, the host also needs to hold SendPort on the sub-isolate. The simplest solution is to create subSendPort in the subISOLATE and pass it back to the host.
void task3(SendPort hostSendPort) {
///5. Create a ReceivePort of the sub-ISOLATE to receive initialization parameters sent from the host
ReceivePort subReceivePort = ReceivePort();
subReceivePort.listen((start) {
if (start is int) {
///9.Computes initialization parameters received from the host.
DateTime startTime = DateTime.now();
int result = fibonacci(start);
DateTime endTime = DateTime.now();
var state =
"Calculation time:${endTime.difference(startTime)}Results:${result.toString()}";
///10. After the calculation, send the result through the host hostSendPort.hostSendPort.send(state); }});///6. Send sendPort of the subisolate to the host for the host to send initialization parameters to the subisolate.
hostSendPort.send(subReceivePort.sendPort);
}
void main() async {
///1 Defines the port from which the host receives the results and the port from which the parameters are sent
ReceivePort hostReceivePort = ReceivePort();
///2 Define the SendPort reference of the subISOLATE
SendPort subSendPort;
///3 Define a listener. The part of the listener will not run for the time being
hostReceivePort.listen((message) {
if (message is SendPort) {
/// 7. The SendPort of the subISOLATE was received, and the bidirectional communication configuration was complete
subSendPort = message;
/// 8. Send initialization data of computing tasks to the subisolate
subSendPort.send(2);
subSendPort.send(10);
subSendPort.send(20);
subSendPort.send(30);
subSendPort.send(40);
subSendPort.send(48);
} else if (message is String) {
/// 11. Print the calculated results in Isolate.
print(message);
} else {
print("Data received does not conform to specification"); }});///4 Define hostSendPort that sends data to hostReceivePort and create the hostReceivePort
SendPort hostSendPort = hostReceivePort.sendPort;
Isolate newIsolate = await Isolate.spawn(task3, hostSendPort);
}
Copy the code
The final output is:
Calculation Time: 0:00:00.000000 Result: 1 Calculation time: 0:00:00.000000 Result: 55 Calculation time: 0:00:00.000000 Result: 6765 Calculation time: 0:00:00.002999 Result: 832040 Calculation time: 0:00:00.393017 Result: 102334155 Calculation time: 0:00:19.179601 Result: 4807526976Copy the code
The above code may seem a bit convoluted in order, but comments have been added to describe the basic running order. It can be seen that to complete a basic two-way communication function, to write a large section of configuration code, really belongs to the business part of the code is a few lines, it is very cumbersome to use.
Simplify the use
The use of Isolate in Flutter is simplified, and bidirectional communication can be easily realized by Compute method.
import 'package:flutter/foundation.dart';
int fibonacci(int n) {
return n < 2 ? n : fibonacci(n - 2) + fibonacci(n - 1);
}
void main()async{
var result = await compute( fibonacci,20);
print(The calculated results are as follows:$result");
}
Copy the code
Output result:
I/ FLUTTER (9899) is calculated to be 6765Copy the code
Reference:
Blog.csdn.net/weixin_3387…
Sg. Jianshu. IO/p/a4a871995…