The isolate
library only gives us the low-level, basic building blocks, so working with isolates in a realistic application environment can be challenging. Specifically for this purpose, the worker
concurrent task executor framework was developed by Diego Rocha, available from pub package manager. It was made to abstract all the isolate managing and message passing and make concurrency in Dart as easy as possible. Worker also contains built-in error handling, so you needn't worry about that either. This recipe will show you how to use this framework so that you can concentrate on higher-level application details.
In the project using_worker
, you can find a number of programs (using_worker1
through using_worker4
) illustrating the use of this framework. The script using_worker.dart
illustrates the main steps, namely creating a task, creating a worker, give a task to the worker, and process the results:
import'package:worker/worker.dart'; Worker worker; void main() { // 1- Make a Task object: Task task = new HeavyTask(); // 2- Construct a Worker object: worker = new Worker(); // specifying poolSize and spawning lazy isolates or not: // worker = new Worker(poolSize: noIsol, spawnLazily: false); // 3- Give a task to the worker // 4- when the results return, process them worker.handle(task).then(processResult); } //5 - Task custom class must implement Task interface classHeavyTask implements Task { execute() { returnlongRunningComputation(); } boollongRunningComputation() { varstopWatch = new Stopwatch(); stopWatch.start(); while (stopWatch.elapsedMilliseconds< 1000); stopWatch.stop(); return true; } } processResult(result) { print(result); // process result // 4- Close the worker object(s) worker.close(); }
First, add the package worker
to pubspec.yaml
, and import it in the code. A task is something that needs to be executed. This is an abstract class in the library, providing an interface for tasks and specifying that your custom Task
class must implement the execute
method, which returns a result. In our script the custom Task
is HeavyTask
, and execute simulates a long running computation using the Stopwatch
class.
A worker
object creates and manages a pool containing a number (poolSize)
of isolates providing you with an easy way to perform blocking tasks concurrently. It spawns isolates lazily as Tasks are required to execute; the spawned isolates are available in a queue named isolates
. The currently active isolates are stored in an iterable called workingIsolates
, and the free isolates can be retrieved from an iterable called availableIsolates
.
In the language of isolates, this is what happens; when a Worker
instance is created, it starts listening to the ReceivePort
of the current isolate, and a pool of isolates is created. The isolates in this pool are used to process any task passed to the worker. When a task is passed to the worker to be handled, it returns a Future. This Future will only complete when the Task is executed or when it fails to be executed.
By default, worker
is created with poolSize
equal to the number of processors on the machine (Platform.numberOfProcessors
), and the isolates are spawned lazily (that is, only when needed). You can, however, change the number of isolates and also whether the isolates are spawned lazily or not using optional constructor parameters, as follows:
worker = new Worker(poolSize: noIsol, spawnLazily: false);
The work is started by handing over the task to the worker with worker.handle(task)
. The handle
method takes a task
, and returns a Future object, so we process the result when it is returned with worker.handle(task).then(processResult);
.
Also, make sure that, after the processing is done, worker
gets closed or the program keeps running.
When executing a number of tasks, you can add them to a List<Future>
task as shown in the following code:
intnoTasks = 500;
for (var i=1; i<=noTasks; i++) {
tasks.add(worker.handle(new HeavyTask()));
}
And then process them with:
Future.wait(tasks).then(processResult);
This mechanism is illustrated in the using_worker2
and using_worker3
examples.
Another good scenario to use isolates or use worker
class in particular is that of a web server that has a few different services. Perhaps one of those services has to do some calculations and takes a while to respond, whereas, the others are light and respond right away. When the heavy service is requested not using isolates, all other requests are blocked, waiting to be processed, even if they are requesting one of the light services. If you do use an isolate or worker and run the heavy service in parallel, it will take roughly the same time to respond to the first request, but all the subsequent requests won't have to wait.