The asynchronous programming model in Dart — Event Loop
First, it should be noted that Dart is a single-threaded language
Event Loop
Dart is a single-threaded programming language. If an operation blocks the Dart thread, the application does not progress until the operation is complete. Therefore, for scalability, it is critical that no I/O operations block. Dart: IO does not block I/O operations, but uses an asynchronous programming model inspired by Node.js, EventMachine, and Twisted.
The above is quoted here
The usual way to implement asynchrony is multithreading, and the other is the Asynchronous programming model (Event Loop).
Event Loop
Dart, like most single-threaded languages, uses Event loops for asynchronous programming, which are good for waiting, time-consuming tasks, as opposed to concurrent programming for computationally intensive tasks
Two queues in the Event Loop
An Event loop contains two Event queues: Event Queue and Microtask Queue.
- Event Queue
- External events:
- I/O
- gestures
- drawing
- The timer
- Steam
- .
- Internal events:
- Future
- External events:
- Microtask Queue
- For very short internal actions that need to be executed asynchronously (such as asynchronous notifications for a Stream)
In most cases, however, we won’t use the Microtask Queue, which is handed over to Dart itself. Microtask Queue has a higher priority than Event Queue
Event Loop execution process
How do I add tasks toMicrotask Queue
andEvent Queue
- Can run directly (not added to any queue)
Future.sync()
Future.value()
_.then()
- Microtask Queue
scheduleMicrotask()
Future.microtask()
_complete.then()
- Event Queue
Future()
Future.delayed()
Timer()
Lay special emphasis on:
_.then() : indicates that the Future is not completed. Then, in this case, is not added to any queue, but executed immediately after the Future is completed
_complete.THEN () : to be used in a Future that has already been completed. Then will be added to the Microtask Queue (since the Future is already completed, its THEN will need to be executed as soon as possible, so it will be added to the Queue with a higher priority).
Future.delayed() : added to Event Queue after delayed Duration
Future.delayed(Duration(seconds: 0),()) will also be added to the Event Queue
Timer() : The internal implementation of the Future is the Timer
The explanation for.then() is provided in the source code comments
If this future is already completed, the callback will not be called, immediately, but will be scheduled in a later microtask.
test_complete.then()
joinMicrotask Queue
In the case
void main() { scheduleMicrotask(() => print('Microtask 1')); Future.microtask(() => print('Microtask 2')); // Since future.value () has already been executed,.then should be completed as soon as possible, Future. Value (1). Then ((value) => print('Microtask 3')); print('main 1'); }Copy the code
Print the result.
flutter: main 1
flutter: Microtask 1
flutter: Microtask 2
flutter: Microtask 3
Copy the code
test_.then()
Immediate execution
Void main() {// If. Then Added to the task queue, the order is delayed -> then 1 -> Microtask -> then 2 // If. Then not added to the task queue, the task must be executed immediately. Delayed -> then 1 -> then 2 -> Microtask Future. Delayed (Duration(seconds: 1),() => print('delayed')) .then((value) { scheduleMicrotask(() => print('Microtask')); print('then 1'); }) .then((value) => print('then 2')); print('main 1'); }Copy the code
Print the result.
flutter: main 1
flutter: delayed
flutter: then 1
flutter: then 2
flutter: Microtask
Copy the code
So the conclusion is correct
Future
A Future is a task that executes asynchronously and completes (or fails) at some point in the Future.
When you instantiate a Future:
- the
Future
An instance of theDartManage the internal array; - Need the
Future
Executed code is pushed directly toEventGo into the ranks; - the
The future instance
Return a state (= _stateIncomplete); - If the next synchronous code exists, execute it (non-future executable code)
As long as the Event Loop retrieves it from the queue, the code referenced by the Future will execute like any other Event. When the code will be executed and will complete (or fail), the THEN () or catchError() methods will be fired directly.
Some very important things to keep in mind:
Futures are not executed in parallel, but follow the sequential rules that event loops handle events.
There are six states of a Future instance:
Static const int _stateIncomplete = 0 // Flag set when no errors need to be handled. Static const int _stateIgnoreError = 1 static const int _stateIgnoreError = 1 Result. static const int _statePendingComplete = 2 Static const int _stateChained = 4 // Complete the status with the value (.then) static const int _stateValue Static const int _stateError = 16 static const int _stateError = 16Copy the code
Async
When you use the Async keyword as a suffix for the method life, Dart will read it as:
- The return value of this method is a Future
- It executes the method synchronously until the first await keyword, and then it suspends the execution of the rest of the method
- Once the Future referenced by the await keyword completes, the next line of code executes immediately
This is important to understand because many developers think that await suspends the whole process until it completes, but this is not the case. They forgot how Event Loop works…
Another thing to keep in mind
Async is not executed in parallel, but in accordance with the order in which events are processed in an event loop.
What are the results of executing method1() and method2()? :
method1() {
List<String> myArray = <String>['a','b','c'];
print('before loop');
myArray.forEach((value) async {
await delayedPrint(value);
});
print('end of loop');
}
method2() async {
List<String> myArray = <String>['a','b','c'];
print('before loop');
for(int i=0; i<myArray.length; i++) {
await delayedPrint(myArray[i]);
}
print('end of loop');
}
Future<void> delayedPrint(String value) async {
await Future.delayed(Duration(seconds: 1));
print('delayedPrint: $value');
}
Copy the code
method1() | method2() |
---|---|
1. before loop | 1. before loop |
2. end of loop | 2. delayedPrint: a (after 1 second) |
3. delayedPrint: a (after 1 second) | 3. delayedPrint: b (1 second later) |
4. delayedPrint: b (directly after) | 4. delayedPrint: c (1 second later) |
5. delayedPrint: c (directly after) | 5. end of loop (right after) |
Method1: Use the forEach() function to walk through arrays of numbers. With each iteration, it calls a new callback function marked async (and therefore a Future). Execute the callback until you encounter await, and then push the rest of the code to the Event queue. Once the iteration is complete, it executes the next statement: “print(‘ end of loop ‘)”. When the execution is complete, the event loop handles the three registered callbacks.
Method2: Everything runs in the same “block” of code, so it can be executed line by line.
Concurrent programming in Dart — Isolate
Dart is a single-threaded language. Is it impossible to do concurrent programming? No,
Dart is a single-threaded language, but concurrent programming is possible using Isolate. The official explanation for Isolate is:
Independent workers that are similar to threads but don’t share memory
The Isolate is not called threads directly, but rather something like threads.
But one way to think about it is,
In Dart, each thread is encapsulated in Isolate. Threads do not share memory, avoiding dead lock. Threads are independent, and garbage collection is efficient. Different isolates communicate with each other through messages.
The difference between Isolate and normal threads
We can see that the isolate is similar to Thread, but in fact the two are essentially different. The main difference is that the ISOLATE does not have shared memory between threads in the operating system.
The relationship is shown below
eachIsolate
Have their ownEvent Loop
(Event loop)
Each Isolate has its own “Event loops” and queues (microtasks and events). This means that code running in one Isolate has no relationship to another.
Start an Isolate
1. Use the underlying implementation
You need to establish communication and manage the newly established Isolate and its life cycle by yourself, which gives you high freedom, but it is relatively difficult to use
Since the ISOLates do not share memory, we need to find a way to establish communication between the caller and the called.
Each Isolate exposes a port called SendPort for passing messages to the other Isolate. The two ISOLates can communicate only if they know each other’s sendPort
The following code example is a two-way process, so both sides need to know sendPort to each other
- CallerReceivePort. SendPort: the caller’s port
- NewIsolateSendPort. SendPort: is the caller’s port
- Create the Isolate and establish communication
- Send a message
- Destruction of Isolate
Void main() async {//1. Create the Isolate and establish a communication. SendPort ReceivePort for retrieving the new ISOLATE callerReceivePort = ReceivePort(); Isolate newIsolate = await createAndCommunication(callerReceivePort); // New ISOLATE SendPort SendPort newIsolatePort = await callerReceivePort.first; Int result = await sendMessage(newIsolatePort, 10000000000); print(result); //3. Release Isolate disposeIsolate(newIsolate); } Future<Isolate> createAndCommunication(ReceivePort callerReceivePort) async {// Initialize new Isolate (spawn() : The first parameter is the entry method for the new ISOLATE, The second parameter is the caller's SendPort) Isolate Isolate = await Isolate the spawn (newIsolateEntry, callerReceivePort. SendPort); return isolate; } sendMessage(SendPort newIsolateSendPort, int num) Async {// Create a temporary port to receive a reply ReceivePort responsePort = ReceivePort(); SendPort newisolatesendport.send ([responsePort.sendport, num]); // Wait for a response and return responseport.first; } void disposeIsolate(Isolate newIsolate) { newIsolate.kill(priority: Isolate.immediate); newIsolate = null; } // New isolate entry (note: NewIsolateEntry (SendPort callerSendPort) Async {// A new instance of SendPort, ReceivePort newIsolateReceivePort = ReceivePort(); / / to the caller provides the isolate SendPort (note: here is communication establish complete) callerSendPort. Send (newIsolateReceivePort. SendPort); // Listen for incoming messages from the caller isolate to the new ISOLATE, and process the calculated returns // Note: Here is to Isolate the main program, and the processing of Isolate newIsolateReceivePort calculated here. Listen ((the message) {/ / processing calculation SendPort port = message [0]. int num = message[1]; int even = countEven(num); // Send the result port.send(even); }); } countEven(int num) {int count = 0; for(var i=0; i<=num; i++){ if(i%2==0){ count ++; } } return count; }Copy the code
Dart2.15 and later
Added the concept of Isolate group
Isolate The Isolate in the Isolate group shares various internal data structures that represent running programs. This makes the individual isolates in the group much more portable. Today, starting an additional ISOLATE in an existing ISOLATE is over 100 times faster than before, and the resulting ISOLATE consumes 10 to 100 times less memory, because there is no need to initialize the program structure
In Dart 2.15, the worker ISOLATE can call isolate.exit (), passing the result as an argument. The Dart runtime then passes the memory data containing the results from the worker ISOLATE to the master ISOLATE without replication, and the master ISOLATE can receive the results for a fixed amount of time. We have updated the compute() utility function in Flutter 2.8 to take advantage of isolate.exit (). If you are already using Compute (), you will automatically get these performance improvements after upgrading to Flutter 2.8.
void main() async { ReceivePort receivePort = ReceivePort(); Isolate.spawn(countTaskInBackground, receivePort.sendPort); int result = await receivePort.first; if (kDebugMode) { print('result = $result'); } } Future countTaskInBackground(SendPort sendPort) async { int result = await countEven(100000000); return Isolate.exit(sendPort, result); } Future<int> countEven(int num) async { int count = 0; for(var i=0; i<=num; i++){ if(i%2==0){ count ++; } } return count; }Copy the code
2. Compute once
You can directly pass in methods and parameters, and manage the Isolate internally. After the method is completed, the Isolate is released directly
- Make an Isolate,
- Running a callback function on the ISOLATE and passing some data,
- Returns the result of processing the callback function,
- After the callback is executed, the Isolate is terminated.
Pay special attention to
Platform-channel communication is supported only by the main ISOLATE. The main ISOLATE corresponds to the ISOLATE created when the application is started.
In other words, programmatically created isolate instances do not allow platform-channel communication. There is another solution -> links
The resources
Github.com/xitu/gold-m…
www.bilibili.com/video/BV12K…
Juejin. Cn/post / 706514…
Mp.weixin.qq.com/s/03729uUAE…