1, Original code
Why Isolate? Let's first look at a relatively simple code:
import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; class TestWidget extends StatefulWidget { @override State<StatefulWidget> createState() { return TestWidgetState(); } } class TestWidgetState extends State<TestWidget> { int _count = 0; @override Widget build(BuildContext context) { return Material( child: Center( child: Column( children: <Widget>[ Container( width: 100, height: 100, child: CircularProgressIndicator(), ), FlatButton( onPressed: () async { _count = countEven(1000000000); setState(() {}); }, child: Text( _count.toString(), )), ], mainAxisSize: MainAxisSize.min, ), ), ); } //Calculate the number of even numbers static int countEven(int num) { int count = 0; while (num > 0) { if (num % 2 == 0) { count++; } num--; } return count; } }
UI It consists of two parts, a continuous circle progress Indicator, a button that, when clicked, finds a positive integer n Even numbers of small numbers (please ignore the specific algorithm and deliberately do time-consuming calculation, ha ha). Let's run the code to see the effect: You can see that it was a very smooth circle. When I click the button to calculate, UI If there is a Caton, why does it happen? Because our calculation defaults to UI In the thread, when we call countEven This calculation takes time, and during this period, UI There is no chance to call the refresh, so it will get stuck. After the calculation is completed, UI Resume normal refresh.
2, Using async optimization
Then some students will say that in dart, there is async keyword, and we can use asynchronous calculation, so it will not affect the UI refresh. Is this really the case? Let's modify the code:
a. take count Change to asyncCountEven
static Future<int> asyncCountEven(int num) async{ int count = 0; while (num > 0) { if (num % 2 == 0) { count++; } num--; } return count; }
b. Call:
_count = await asyncCountEven(1000000000);
Let's continue to run the code to see the phenomenon:
Still stuck, indicating that asynchrony can't solve the problem. Why? Because we still perform operations in the same UI thread, asynchrony only means that I can run other operations first and return them when there are results on my side. However, remember, our calculations are still in this UI thread and will still block UI refresh. Asynchrony is only a concurrent operation in the same thread.
3, Optimize with compute
So how can we solve this problem? In fact, it is very simple. We know that the cause of Caton is caused by the same thread. Is there any way to move the calculation to a new thread? Of course, it is possible. However, in dart, this is not called thread, but Isolate, which is called isolation. Such an odd name is because isolation does not share data. The variables in each isolation are different and cannot be shared with each other.
However, due to the heavy weight of Isolate in dart and the complexity of data transmission in UI threads and Isolate, in order to simplify user code, fluent encapsulates a lightweight compute operation in the foundation library. Let's look at compute first and then Isolate.
To use compute,There are two points that must be paid attention to. One is our compute The function running in must be a top-level function or static Function, the second is compute For parameter passing, only one parameter can be passed, and there is only one return value. Let's take a look at the parameter in this example first compute Optimize it: It's really simple. It's only used when it's in use compute Function.
_count = await compute(countEven, 1000000000);
Run again, let's see the effect: It can be seen that the current calculation will not lead to UI Carton, perfect solution.
4, Optimize with Isolate
However, there are still some restrictions on the use of compute. It can neither return results multiple times nor transfer values continuously. Each call is equivalent to creating a new isolation. If there are too many calls, it will be counterproductive. In some businesses, we can use compute, but in other businesses, we can only use Isolate provided by dart. Let's take a look at the use of Isolate in this example:
a. Add these two functions
static Future<dynamic> isolateCountEven(int num) async { final response = ReceivePort(); await Isolate.spawn(countEvent2, response.sendPort); final sendPort = await response.first; final answer = ReceivePort(); sendPort.send([answer.sendPort, num]); return answer.first; } static void countEvent2(SendPort port) { final rPort = ReceivePort(); port.send(rPort.sendPort); rPort.listen((message) { final send = message[0] as SendPort; final n = message[1] as int; send.send(countEven(n)); }); }
b. use
_count = await isolateCountEven(1000000000);
be relative to compute It's a lot more complicated, the effect won't be posted, and compute Same, no Caton..
What's the price
For us, multithreading is actually used as a computing resource. We can calculate the heavy work by creating a new isolate, so as to reduce the burden of UI threads. But what is the cost?
time
Generally speaking, when we use multithreaded computing, the whole computing time will be more than that of single thread. What is the additional time?
- Create Isolate
- Copy Message
When we execute a piece of multithreaded code according to the above code, we go through the process of creating and destroying isolate. Here is a possible way for us to write code like this in parsing json.
static BSModel toBSModel(String json){} parsingModelList(List<String> jsonList) async{ for(var model in jsonList){ BSModel m = await compute(toBSModel, model); } } Copy code
When parsing json, we may complete the parsing task in a new isolate through compute, and then pass the value. At this time, we will find that the whole parsing will become abnormally slow. This is because each time we create a BSModel, we go through the process of creating and destroying an isolate. This will take about 50-150ms.
In this process, we also pass data through network - > main isolate - > new isolate (result) - > main isolate, and there are two more copy operations. If we download data from an isolate other than the main thread, we can parse it directly in the thread. Finally, we only need to return the main isolate, saving a copy operation. (Network -> New Isolate (result)-> Main Isolate)
space
Isolate is actually heavy. Every time we create a new isolate, it needs at least 2mb of space or more, depending on our specific purpose of isolate.
OOM risk
We might use message to pass data or file. In fact, the message we deliver has gone through a copy process, which may involve the risk of OOM.
If we want to return a 2GB of data, we cannot complete the message delivery operation on iPhone X (3GB ram).
Tips
It has been described above that using isolate for multithreading operation will have some additional costs, so can we reduce these costs by some means. I personally suggest starting in two directions.
- Reduce the consumption caused by isolate creation.
- Reduce the number and size of message copies.
Using LoadBalancer
How to reduce the consumption caused by isolate creation. A natural idea is whether to create a thread pool and initialize there. Just use it when we need it.
In fact, dart team has written a very practical package for us, including LoadBalancer.
We now add the dependency of isolate to pubspec.yaml.
isolate: ^2.0.2 Copy code
Then we can create a specified number of isolate s through the LoadBalancer.
Future<LoadBalancer> loadBalancer = LoadBalancer.create(2, IsolateRunner.spawn); Copy code
This code will create an isolate d thread pool and automatically realize load balancing.
Since dart naturally supports top-level functions, we can directly create this LoadBalancer in the dart file. Let's take a look at how to use isolate in LoadBalancer.
int useLoadBalancer() async { final lb = await loadBalancer; int res = await lb.run<int, int>(_doSomething, 1); return res; } Copy code
We only focus on future < R > Run < R, P > (futureor < R > function (P argument), argument, method. We still need to pass in a function to run in an isolate and pass in its parameter argument. The run method will return the return value of the method we execute.
The overall use feels similar to that of compute, but when we use additional isolate multiple times, we don't need to create it again.
In addition, LoadBalancer also supports runMultiple, which allows a method to execute in multiple threads. Please check the api for specific use.
The LoadBalancer is tested and initializes the thread pool the first time it uses its isolate.
When the application is turned on, even if we call LoadBalancer.create in the top-level function, there will still be only one Isolate.
When we call the run method, we really create the actual isolate.
Author: San Ye horizon
Link: https://www.jianshu.com/p/07b19f4752ea
Source: Jianshu
The copyright belongs to the author. For commercial reprint, please contact the author for authorization. For non-commercial reprint, please indicate the source.