Chapter 8. Asynchrony and Isolates

We have focused on sequential Dart programs until now. However, most real Dart programs are concurrent.

A web application usually consists of at least two concurrent parts—a client program running in a web browser on a user’s device, and a server program running outside the browser, usually on another computer somewhere across the internet. Many server side programs service many clients simultaneously. These are typical scenarios, but there are many others.

In this chapter, we shall turn our attention to the those features of Dart that support concurrency and distribution. Dart concurrency is based on isolates (8.4), which in turn are built upon futures (8.2) and streams (8.3).

8.1 Asynchrony

When one invokes a method or calls a function in Dart, the caller waits until the callee has been evaluated and returns a result. While the callee is executing, the caller is blocked. A function call is much like a phone call, where the caller waits for the other party to answer. Calling is a form of synchronous communication—caller and callee synchronize their activity in time.

In contrast, in asynchronous communication the caller initiates an action but does not wait for it to complete, instead continuing execution immediately. Examples of situations where asynchronous communication might be appropriate include tasks like input or output, where the action requested might be slow and waiting for the action to complete is undesirable.

Asynchrony is fundamental to Dart’s approach to concurrency. Dart supports asynchrony both within a single isolate and across multiple isolates.

Cross-isolate communication is based on asynchronous message passing. In its basic form, message-passing is a “fire-and-forget” activity. The sender has no assurance that the message was received or that a response will ever be given. This is analogous to sending a letter by old-fashioned post. One can initiate some action by sending a message, but one does not know if the action was carried out, and if so if it was successful or not. One certainly cannot tell what the resulting value was in case of success, nor what error was thrown in case of failure. This is unsatisfactory. Applications need to be able to monitor and respond to the asynchronous activities they initiate. To address this need, we use futures.

8.2 Futures

A future is an object that represents the result of a computation that may not yet have taken place. The result may be known at some future time, hence the name “future.” Futures are useful in asynchronous computation. Once a request to perform an asynchronous computation is made, a future can be returned immediately, in a synchronous fashion, and the actual computation can be scheduled for a later time. The future gives the code requesting the asynchronous operation a handle, a reference to the requested computation. After the requested computation is completed, the future can be used to get access to the result. We say that the future has been completed, or alternatively, has been resolved.

In Dart, futures are realized by the class Future in the dart:async library. Future is a generic class (5.5), with type parameter T representing the type of value the future will be completed with.

8.2.1 Consuming Futures

Futures have an instance method then() that takes a closure, onValue, as a parameter. This closure will be called when the future completes successfully. The closure itself takes a parameter of type T, the type of the computation the future represents. So, when the future is complete, the value it represents will be passed to onValue.

As an example, suppose we have a file that we wish to copy. File1 objects support an asynchronous API for copying files:

1. Here we refer to the File class defined in dart:io, as opposed to the one defined in dart:html which has a different API altogether.

Future<File> copy(String newPath)

If we call copy() on a File, we get a future back. An advantage of the asynchronous API is that one can initiate a copy of a large file and not wait for the copying to complete. The downside is that if we actually want to do something with the copy, things get a bit more complicated because we must interact with futures.

A simple test might assert that if we have a file and make a copy at a path myPath, the resulting file’s path is indeed myPath. Because the result of copy() is a future on a file rather than the file itself, we can’t just write

assert(myFile.copy(myPath).path == myPath);

The correct code is

myFile.copy(myPath).then((f){ assert(f.path == myPath);});

After the file system is done copying, the future returned by copy() will be completed with an actual File object, and the closure passed into then() will be called with said File instance, causing the assertion to fire. By that time, Dart execution will have long since moved on.

What if a future f1 represents a computation that results in another future f2? It won’t do to have f1 complete if the result is itself a future. We would call onValue but be forced to test if the incoming argument was a future and deal with that via another then() and so on ad infinitum. Instead, in such cases the completion of f1 is made dependent on the completion of f2. When f2 is completed with a non-future value, f1 will complete with the same value. We say the futures are chained because they form a dependency chain.

Futures can represent failed computations as well. The method catchError() is also defined on futures, and it too takes a closure as input. The closure, onError, is invoked if the computation the future represents throws an exception.

8.2.2 Producing Futures

In principle, code that needs to return a future to its caller must do several things. It must schedule a computation to be executed at some later time, it must instantiate an instance of Future and it must associate that instance with the scheduled computation, so that when the scheduled computation is done, the future will be completed with the result. Care must be taken to catch any exceptions the scheduled computation throws and complete the future with the appropriate errors.

The constructors of Future manage all this bookkeeping for us. The code looks roughly like this:

Future(Function computation){
 Timer.run(() =>
   try {
     _completeWithValue(computation());
   }
   catch (e){
     _completeWithError(e);
 });
}

The class Timer, defined in dart:async, has a static method run() that schedules code for execution. The Future constructor takes a closure computation embodying the computation the user wants to schedule. The actual job scheduled, however, is a closure designed to capture the result of computation, successful or otherwise, and complete the instance of Future being constructed as appropriate. The completion is done via private methods such as completeWithValue(). We do not want to expose these to the consumers of futures; such consumers need not, and should not, be able to influence the results of the future instance returned to them.

The net effect is that the Dart programmer simply writes something like:

new Future(myComputation);

where myComputation defines the computation desired.

8.2.3 Scheduling

When we say that code is scheduled for future execution, what do we mean? A Dart application has an event loop that is run as long as there are event handlers available to process ongoing events. Event handlers target a variety of events—mouse clicks, keyboard input and others. Of particular interest to us are timer events—events that mark the passage of time.

The class Timer mentioned in the previous section is used to define handlers for timer events. One can define a handler that is designed to trigger after a given time period has elapsed. An important special case is when that time period is zero. A handler that triggers when zero time has passed is simply a task that we want to run unconditionally at the earliest opportunity. One can define such a handler using Timer.run(). Setting up such a handler amounts to scheduling a task for future execution, to be run in a subsequent iteration of the event loop.

It is possible to schedule a future to execute after a non-zero delay, via the constructor Future.delayed(). See the Dart API documentation for details on this and the several other constructors for futures.

Once a task is started, it runs to completion; tasks are never preempted. The period in which a task is run is called a turn. All futures scheduled within a turn are guaranteed to be run only after the turn is finished—except for microtasks, described below.

8.2.3.1 Microtasks

As a rule, we must also assume that other code might execute before a task we have scheduled via a future. That code might be

• Code from another isolate

• An event handler associated with an event such as a mouse click

• The computation associated with other futures scheduled previously

Any one of the above kinds of tasks could be run by the event loop once the current turn is over, before any future scheduled by the current turn gets to run. Sometimes, we really need to run some asynchronous action before the event loop resumes.

There is a mechanism for ensuring that a future runs before any code scheduled outside our turn. Each turn has its own microtask queue. Tasks on the microtask queue are run at the end of the turn before returning control to the main event loop.

We can schedule futures to run on the microtask queue using the constructor Future.microtask(). It is important to keep microtasks few and short.

8.3 Streams

A stream is a list of values of indeterminate length. It may be infinite, going on forever. Or it may end eventually, but the point is that we don’t know when or if the stream ends.

Examples of streams might be the position of the mouse over time, or a list of all prime numbers, or a live video being sent to us over the network.

One may subscribe (or listen) to a stream, meaning that one registers one or more callback functions with the stream. These functions will be called when new data is appended to the stream.

It is natural to view a stream as a collection. Many operations on collections apply to streams. One can invoke map() on a stream and thereby derive a transformed stream of computed results. For example, we can map a pair of coordinates to an image translated by those coordinates, and thus build an application that moves an image around the scene in response to mouse movements.

A stream derived using map() is 1:1 with the original stream from which it is derived. Sometimes it is useful to be able to replace an element of the stream with multiple elements. As a special case, if that multiple is 0, we can filter out elements. The expand() operation (known as flatMap() in many other languages) allows us to do so.

Filtering elements can be done using expand() but it is easier to use where().

Some operations on collections need to traverse the entire collection. In the case of streams, we cannot do so immediately, so these operations return futures. For example, suppose we had a stream primes of all prime numbers and we wanted to sum them up:

Future<int> sisypheanSum = sum(primes);
sisypheanSum.then((s) => print(s));

Of course, this is a rather Sisyphean task if our stream of primes really keeps running forever. If our stream does shut down at some point, then we will get a result. What is nice is that the same sum() function we used back in Section 4.4 will work unchanged with a stream.

8.4 Isolates

Dart supports actor-style concurrency. A running Dart program consists of one or more actors, known as isolates. An isolate is a computation with its own memory and its own single thread of control. The term isolate derives from the isolation that exists between isolates, because an isolate’s memory is logically separate from the memory of any other isolate. Code within an isolate runs sequentially; any concurrency is a result of running multiple isolates. Hence there is no shared memory concurrency in Dart, and therefore no need for locks and no possibility of races.

Since isolates have no shared memory, the only way that isolates can communicate is via message passing. Message passing in Dart is always asynchronous.

Unlike some languages, isolates have no blocking receive construct. As a result, deadlock cannot occur.

8.4.1 Ports

An isolate has several ports. Ports are the low-level foundation upon which Dart’s inter-isolate communication is built. Ports are of two kinds: send port and receive ports.

A receive port is a stream (8.3) of incoming messages. A send port allows messages to be sent to an isolate; more specifically, it allows messages to be posted to a receive port. Receive ports can manufacture send ports that post any messages sent on them to their originating receive port. We’ll see how this works in the next subsection.

8.4.2 Spawning

Starting up one isolate from another is known as spawning. An isolate begins execution at the main() method of a library that is specified when the isolate is spawned. This library is called the root library of the isolate.

The class Isolate provides two class methods for spawning. The first is spawnUri(), which spawns an isolate based on a library given via a URI. The second is spawn(), which produces an isolate based on the current isolate’s root library.

A Dart program begins execution in the main isolate, spawned by the Dart runtime. In order to create new isolates, the code running in the main isolate will have to spawn them. When an isolate spawns another, it has the opportunity to pass some initial arguments. The most critical such argument is an initial message. Messages are defined inductively: a message is either null, a number, a Boolean, a string, a send port, a list of messages or a map from messages to messages. The initial message typically includes a send port that the newly spawned isolate (the spawnee) will use to send messages back to the isolate that spawned it (its spawner).

How does the spawner produce a send port to pass to its spawnee? The spawner creates its receive port r1, extracts a send port s1 from it and spawns a new isolate, passing in the new send port:

main() { // in the main isolate
 ReceivePort r1 = new ReceivePort();
 SendPort s1 = r1.sendPort;
 Isolate.spawnUri(new Uri(path:'./otherIsolate.dart'), [], s1);
}

The spawnee then creates its own receive port r2, and extracts a send port s2 from it, which gets sent back to the spawner via s1. This mating dance results in a pair of isolates that can communicate with each other:

main(args, SendPort s1) { // in otherIsolate.dart
 ReceivePort r2 = new ReceivePort();
 SendPort s2 = r2.sendPort;
 s1.send(s2);
}

The process just described is a bit tedious, though it only amounts to three lines of ritualistic code at the start of each isolate. However, the port mechanism is sufficiently flexible that various higher-level mechanisms can be built on top of it, as we shall soon see.

8.4.3 Security

Isolates are the foundation of security in Dart. Memory isolation prevents one isolate from impacting the state of another. As a result, the language’s privacy constructs have no security implications, and serve only software engineering purposes.

One consequence is that one can violate the privacy of libraries via reflection (7), and that reflection is unrestricted within the confines of an isolate. Cross-isolate reflection is another matter.

8.5 Example: Client-Server Communication

In this section, we will show how to structure a web application in a way that is independent of the specific mechanics of the web browser. We will view the application as a pair of communicating isolates, one representing the server and one representing the client.

We will take the view that the server is itself an object with an interface of operations it makes available to the client. These operations will all be asynchronous, returning futures. However, the futures returned will be of a special kind we shall now define, which we shall call promises.

8.5.1 Promise: A Brighter Future

Promises are designed to mitigate the pain of interacting with asynchronous code. Promises handle the boilerplate of call-backs on futures automatically. Using noSuch-Method(), a promise transforms method invocations made on it into callbacks using then().

When a method invocation returns a promise, one can immediately invoke further methods on the promise, almost as if it was the actual result. This alleviates much of the tedium of using futures, as code is no longer full of deeply nested callback declarations.

library promise;
import 'dart:async' show Completer, Future;
import 'dart:mirrors' show InstanceMirror, reflect;
@proxy class Promise {
 Future future;
 Promise(this. future);
 noSuchMethod(Invocation inv) {
  onValue(v) {
   InstanceMirror m = reflect(v);
   var result = m.delegate(inv);
   return result;
   }
  return new Promise( future.then(onValue));
  }
}

class Resolver {
 Completer completer = new Completer();
 Promise _promise;
 get promise {
  return promise == null ? _promise = new Promise(_completer.future): _promise;
 }
 resolve(v) {
  _completer.complete(v);
 }
}

8.5.1.1 Limitations: Promises We Can’t Keep

The code above suffers from the same problem we saw before for general-purpose proxies (5.7.6). The difference between a promise and the actual value will be detected by dynamic type checks—both explicit checks and casts (via is and as) and implicit checks in checked mode.

The process also breaks down when the results are needed by control constructs such as conditionals and loops, or for output. Compare printing of a known value v to printing a promise p for v:

print(v); // prints the value v
print(p); // prints something like 'An instance of Promise'

The immediate problem here is that print() relies on toString(), which is inherited from Object. A more thorough forwarding mechanism would override all the methods of Object and forward them, via then() to the future. However, that doesn’t really solve the problem in this case; instead it aggravates it. If print() gets a Promise back from toString() instead of a string as expected, it will throw an exception.

Even more acute is the issue of control flow. If we have an asynchronous predicate, we cannot use its result to make a control flow decision in a conditional.2

2. The issues with respect to output and control flow are fundamentally the same. These are in essence strict functions that will not accept lazy inputs.

We could try adding methods that emulate conditionals, essentially deferring the decisions on control flow until the promise is resolved. However, these methods will not properly handle constructs like break, continue and return.

Finally, there is some concern over performance. Calls using noSuchMethod() may be significantly slower than normal calls and so one needs to take care that they are not used in performance critical code. This should not be a concern for the scenario we have in mind, where remote isolates are communicating. A potentially more serious worry is that using noSuchMethod() makes code harder to analyze statically, which tends to increase code size when compiling to Javascript.

Despite all the above problems, promises can be useful in certain circumstances. In the following sections, we shall look at using promises in the context of client-server communication.

8.5.2 Isolates as Distributed Objects

Our next step is to provide a view of isolates as objects. We will need to define the service API as a Dart class, write code to run the service from a client, and of course implement the service. Last but not least, we’ll need to build the infrastructure used by the service and its clients.

8.5.2.1 Service API

Assume we have a service that lets people send cookies to a given address. The API for this service is very simple, and is given by the following abstract class:

abstract class CookieService {
  Receipt sendCookiesTo(recipientAddress, customer);
}

You tell the service where to send the cookies, and give them your details so they can charge you. The service returns a receipt once it processes the request (they charge you right away, not when the cookies ship).

Object Serialization

Our model relies on sending objects between client and server, which entails object serialization. We assume that all objects being serialized are instances of types that are known to both the client and the server. We also assume these types are exactly the same on both ends of the connection.

8.5.2.2 Running the Service

To run the service, we define our main isolate via the library:

library drive cookie service;

import 'civilisolates.dart' as civilisolates show startServiceObject;

We rely on the library civilisolates to provide the necessary infrastructure, specifically the method startService used below:

main() {
 var cookieService =
          civilisolates.startServiceObject(new Uri(path: 'cookie service.dart'));
 cookieService.sendCookies(getAddress(), getCustomerDetails());
}

The code above starts up a service based upon the library at cookie service.dart using startServiceObject. The effect of startServiceObject is to start up an isolate based on the library at the URI passed to it, and return a promise that allows us to communicate with the new isolate.

Next, we invoke sendCookies() on the promise, which we stored in the local variable cookieService. The service isolate probably hasn’t even started up yet, but no matter; because startServiceObject returns a promise, not just a plain future, we can immediately invoke the method sendCookies() on it.

All the client needed to do is reference the service via a URI. After that, it talks to the service as an ordinary object. As we’ll see, the situation on the server side is also quite simple.

8.5.2.3 Service Implementation

To implement our service, we define cookie service.dart as follows:

library cookie service;

import 'civilisolates.dart' as civilisolates show serve;

class MyCookieService implements CookieService {
 Receipt sendCookiesTo(recipientAddress, customer){
  charge(customer); // first things first
  initiatePhysicalShipment(recipientAddress);
  return new Receipt(customer);
 }
}

main(args, client) {
 CookieService obj = new MyCookieService();
 civilisolates.serve(obj, client);
}

Again, we import civilisolates to provide necessary infrastructure. The cookie service library defines a class MyCookieService with a set of operations that define the API of the service the library provides. To make the library act as a service, we instantiate MyCookieService and pass it to civilisolates.serve(). The call to serve() will make the currently running isolate listen for messages from client, convert them into method invocations on service, and send the serialized results back; in short, serve makes the current isolate act as a service implemented by obj.

Almost all our code is independent of asynchrony. We defined the functionality needed in an ordinary class. On the server side, only main() is slightly special—but it is mercifully brief and formulaic.

Of course, this picture is a bit oversimplified. Our promises don’t work as smoothly if we have complex control flow that relies on results from an asynchronous call. We do have to design the API of our isolate at a suitable granularity to avoid these problems and to minimize round trips. But we have avoided a great deal of boilerplate code.

8.5.2.4 Infrastructure

Now let’s look at the infrastructure we’ve been using to define and launch our service. This includes the definitions of startService and serve. It’s all defined in the civilisolates library.

library civilisolates;

import 'dart:isolate' show Isolate, ReceivePort, SendPort;
import 'dart:async' show Completer, Future;
import 'dart:mirrors' show InstanceMirror, reflect;
import 'promise.dart' show Promise, Resolver;
import 'serializer.dart' as serializer show serialize;
import 'deserializer.dart' as deserializer show deserialize;

msg2Obj(List<String> msg) => deserializer.deserialize(msg);
List<String> obj2Msg(obj) => serializer.serialize(obj);

We’ll make use of the serializer we introduced in Section 7.1.3. A more robust realization of these ideas would of course require better serialization support, but what we have suffices to illustrate the main ideas.

Below, you can see how serve helps an isolate represent an object (obj) as a service to a given client (represented via the send port client). It sets up the necessary ports to communicate with the client. It then listens for any incoming messages and forwards them using reflection to obj, sending the results back to the client:

void serve(obj, SendPort client) {
  // send client our address
  ReceivePort rport = new ReceivePort();
  SendPort selfPort = rport.sendPort;
  client.send(selfPort);
  // set up listener to respond to sends from client
  InstanceMirror target = reflect(obj);
  rport.listen (
    (msg){
      var invArgs = msg[0];
      SendPort returnAddress = msg[1];
      // forward invocation to obj
    var result = target.invoke(invArgs[0], msg2Obj(invArgs[1]), msg2Obj(invArgs[2]));
      // extract and serialize result and send back to client
      returnAddress.send(obj2Msg(result.reflectee));
    }
  );
}

Next, startServiceObject is used by clients to set up an object representing a service. The service will be implemented by a library residing at uri. We will use an isolate that will run our service. We begin with the mechanics of hooking up ports and spawning the service isolate. We want to create a proxy object for the isolate, but the isolate is created asynchronously. All we have is a future for a send port for the isolate. And so, we return a promise for the proxy instead. When the future for the port completes, it will trigger creation of the proxy:

Promise startServiceObject(uri) {
 ReceivePort rport = new ReceivePort();
 SendPort selfPort = rport.sendPort;
 // start up the service isolate
 Isolate.spawnUri(uri, [], selfPort);
 // When we get the isolates address back
 // we can set up a proxy object for it. In the meantime,
 // return a promise for the proxy
   Future<SendPort> target = rport.elementAt(0);
   return new Promise(target.then((s) => new IsolateProxy(s)));
}

The last piece of the puzzle is the proxy for isolates. It works similarly to the proxies we’ve seen before. The difference is in what the noSuchMethod routine does. It has to create a port specifically to listen for the answer to the method invocation it is currently handling. Each invocation gets its own port because we don’t want to mix up the answers as they come back.

The invocation is serialized and sent to the target, the isolate being proxied. We return a promise for the result. We set up a listener to await the answer; the handler will resolve the promise to the deserialized answer when it arrives.

As a simplification we choose not to handle named parameters. Our example doesn’t call for them, and our sample serialization code doesn’t support maps:

class IsolateProxy {
 final SendPort _sendPort;
 IsolateProxy(this._sendPort);
 noSuchMethod(Invocation inv) {
  ReceivePort rport = new ReceivePort();
  // create a dedicated port to receive the answer for this invocation
  SendPort selfPort = rport.sendPort;
  Resolver resolver = new Resolver();
  // When the answer comes, we'll be listening
  rport.listen((answer){
    // deserialize the answer;
    // first element of serialized form is the desired result
    var a = msg2Obj(answer)[0];
    // resolved promise accordingly
    resolver.resolve(a);
    // and shut down the port dedicated to this invocation
    rport.close();
  });
  // serialize the invocation and send it to the target isolate
  _sendPort.send([obj2Msg([inv.memberName, inv.positionalArguments]), selfPort]);
  return resolver.promise;
 }
}

One can of course create a proxy directly given a send port for an isolate; one doesn’t have to use startServiceObject. It is merely a convenience in the case where we start up the service for the first time.

Unfortunately, chained promises as illustrated have limited utility in Dart for all the reasons cited in Section 8.5.1.1 above. To address these limitations, Dart has dedicated support for asynchrony at the language level, as we’ll show next.

8.6 Asynchronous Functions

As we’ve seen, working directly with futures can be awkward. Part of the problem is that the classic control structures we are familiar with were not designed with asynchrony in mind. Once an asynchronous call has been made, all the bookkeeping necessary to track whether the call has executed, whether execution was successful or not and what were the results, becomes the responsibility of the programmer. The work of scheduling futures, and scheduling work to be done when that work is done, successfully or not, is rather onerous.

To ease the pain of working with asynchrony, Dart provides language-level support for asynchronous functions. A function body can be marked with the async modifier; the function is then an async function:

Future<int> foo() async => 42;

Using async functions can simplify the task of working with futures and asynchrony in several ways.

When an async function is called, the function’s code is not executed immediately. Instead, the code in the function gets scheduled for execution at some future time. So what gets returned to the caller? A future that will be completed when the function’s body is done, successfully or not. The function manufactures the future automatically and immediately returns that future to the caller.

Clearly, this relieves the programmer from writing a certain amount of boilerplate code, but the real value of async functions is that they can contain await expressions.

8.6.1 Await

An await expression allows one to write asynchronous code almost as if it were synchronous. Executing an await expression lets us suspend the surrounding function while we wait for an asynchronous computation to finish.

Recall our example from Section 8.2.1, where we wanted to test that the copy() method of File worked as advertised. Using await, we can write the assertion as:

assert((await myFile.copy(myPath)).path == myPath);

which is very close to what we would write if copy was synchronous:

assert(myFile.copy(myPath).path == myPath);

and more direct than the version using futures explicitly:

myFile.copy(myPath).then((f){ assert(f.path == myPath);});

An await can only be used within an async function. The Dart compiler will not accept an await elsewhere.

When thinking about the execution of an async function, one needs to remember that its body has been scheduled for execution independently of its caller, in a later turn. So when the async function is running, its caller is long gone. In particular, when debugging, there is no meaningful call stack to refer to—and there is no caller to return to. So what does a return statement mean inside an async function? All a return can do is complete the future associated with an async function with a value. As usual, a return without a nested expression is treated as a shorthand for return null;. Likewise, if an async function throws an exception that it does not catch, the function’s future is completed with the object thrown.

8.6.2 Asynchronous Generators

Asynchronous generators are another form of asynchronous function supported by Dart. A function whose body is marked with the async* modifier acts as a generator function (4.8) for a stream. The idea is best illustrated via an example. The following function produces a stream containing the natural numbers in sequence:

get naturals async* {
 int k = 0;
 while (true) {
  yield await k++;
 }
}

When naturals is called, it immediately returns a new stream. Once that stream is listened to, the function body is run in order to generate values to populate the stream. The function runs in an infinite loop. Each iteration executes a yield statement, which has an await inside it. The await increments k and suspends the function.

Later, the function is resumed with a new value of k, which yield appends to the stream. This process repeats on every iteration, adding the next natural number to the stream. Of course, this isn’t a very good way to produce this sequence; the function is run at most once every cycle through the event loop.

Here is a more realistic example, which obtains data in chunks over HTTP and places it on a stream. The function getRepeatedly is called with a URI uri and a number n indicating how many times the URI should be accessed:

Stream getRepeatedly(uri, n) async* {
 for (var i = 0; i < n; i++) {
    var response = await http.get(uri);
    var nextJson = json.convert(response.body);
    yield nextJson;
    await new Future.delayed(new Duration(seconds: 2));
 }
}

A stream is returned to the caller immediately. Once the stream is listened to, the body of getRepeatedly will begin running. It will loop n times; each iteration will access the URI provided using await to obtain a response asynchronously and suspend. After the response is ready, the function will resume. The response is converted to JSON and posted to the stream that was returned to the caller. Then the function waits for two seconds and starts the next iteration.

8.6.3 Await-For loops

Given a stream, one can loop over its values:

await for (var i in naturals) { print('event loop $i'), }

Every time an element is added to the stream, the loop body is run. After each iteration, the function enclosing the loop suspends until the next element is available or the stream is done.

Just like await expressions, await-for loops may only appear inside asynchronous functions. The reasoning in both cases is the same: both constructs cause the surrounding function to suspend, and a function that suspends is no longer synchronous.

8.7 Related Work

Dart’s model of isolates with their own memory is a variant of the actor model first proposed by Hewitt over 40 years ago[22], [23]. Since then, many variations on the actor model have been introduced in various languages, notably Erlang[24], E[25], Newspeak[26] and Scala[27]. As in E, Newspeak and the original actor model, Dart message passing is non-blocking.

The concept of a future in programming has many variants. These include the promises of E, the futures of Scala’s actor library Akka and many others.

Dart’s streams are heavily influenced by the work of Meijer on Rx[28]. Support for async methods and await expressions was inspired by the analogous constructs in C#. However, unlike C#, Dart async functions are always asynchronous, and Dart’s await expressions always suspend the surrounding function.

8.8 Summary

Dart programs consist of one or more isolates, which are actor-like units of concurrency, each with its own memory and a single thread of control. Isolates are Dart’s unit of concurrency and security. Isolates are non-blocking and communicate via asynchronous message passing. Message sends return futures representing their anticipated results. Futures support callbacks that specify what action to take when the future completes, either successfully or unsuccessfully. Streams provide a higher-level abstraction on which Dart applications are typically constructed. Finally, async methods make it possible to write asynchronous programs in a style not dissimilar to synchronous ones.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.138.104