User Tools

Site Tools


at:tutorial:actors

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
Next revisionBoth sides next revision
at:tutorial:actors [2007/07/18 10:09] elisagat:tutorial:actors [2008/11/03 10:08] – * tvcutsem
Line 26: Line 26:
   };   };
 }; };
->><far ref to:<object:1555668>>+>><far ref:behaviour of <actormirror:9501984>>
 </code> </code>
  
Line 70: Line 70:
   };   };
 }; };
->><far ref to:<object:11600335>>+>><far ref:behaviour of <actormirror:14115383>>
 </code> </code>
  
Line 112: Line 112:
   };   };
 }; };
->><object:15603573[<type tag:Isolate>]>+>><obj:{super,super:=,re,re:=,im,im:=,...}[Isolate]>
 </code> </code>
  
Line 172: Line 172:
 ===== Futures ===== ===== Futures =====
  
-As you may have noticed previously, asynchronous message sends do not return any value (that is, they return ''nil''). Quite often, the developer is required to work around this lack of return values by means of e.g. explicit customer objects, as shown previously. This, however, leads to less expressive, more difficult to understand code, where the control flow quickly becomes implicit.+As you may have noticed previously, asynchronous message sends do not return any value (that is, they return ''nil''). Quite often, the developer is required to work around this lack of return values by means of e.g. explicit customer objects, as shown previously in the calculator example. This, however, leads to less expressive, more difficult to understand code, where the control flow quickly becomes implicit.
  
 ==== The Concept ==== ==== The Concept ====
  
-The most well-known language feature to reconcile return values with asynchronous message sends is the notion of a [[Wp>Future_(programming)|future]]. Futures are objects that represent return values that may not yet have been computed. Once the asynchronously invoked method has completed, the future is replaced with the actual return value, and objects that referred to the future transparently refer to the return value.+The most well-known language feature in concurrent and distributed languages (for example, in ABCL, the actor-based concurrent language) to reconcile return values with asynchronous message sends is the notion of a  [[Wp>Future_(programming)|future]]. Futures are also commonly known by the name of //promises// (this is how they are called in the [[http://www.erights.org|E language]] and in Argus). Futures are objects that represent return values that may not yet have been computed. Once the asynchronously invoked method has completed, the future is replaced with the actual return value, and objects that referred to the future transparently refer to the return value.
  
 Using futures, it is possible to re-implement the previous example of requesting our calculator actor to add two numbers as follows: Using futures, it is possible to re-implement the previous example of requesting our calculator actor to add two numbers as follows:
Line 186: Line 186:
 ==== Enabling futures ==== ==== Enabling futures ====
  
-Futures are a frequently recurring language feature in concurrent and distributed languages (for example, in ABCL, the actor-based concurrent language). They are also commonly known by the name of //promises// (this is how they are called in the [[http://www.erights.org|E language]] and in Argus). In AmbientTalk, futures are not native to the language. However, because of AmbientTalk's reflective infrastructure, it is possible to build futures on top of the language. The system library shipped with AmbientTalk contains exactly this: a reflective implementation that adds futures to the language kernel. This implementation can be found in the file ''at/lang/futures.at''.+In AmbientTalk, futures are not native to the language. However, because of AmbientTalk's reflective infrastructure, it is possible to build futures on top of the language. The system library shipped with AmbientTalk contains exactly this: a reflective implementation that adds futures to the language kernel. This implementation can be found in the file ''at/lang/futures.at''.
  
 To enable futures, it suffices to import the futures module and to enable it, as follows: To enable futures, it suffices to import the futures module and to enable it, as follows:
Line 196: Line 196:
  
 The first statement imports the futures module into the current lexical scope. This enables you as a developer to use some additional language constructs exported by the futures module, as will be explained later. The second statement enables the futures behaviour, causing any asynchronous message send to return a future rather than ''nil''. If ''false'' is passed to the call to ''enableFutures'', only messages marked explicitly as a ''FutureMessage'' will return a future. The first statement imports the futures module into the current lexical scope. This enables you as a developer to use some additional language constructs exported by the futures module, as will be explained later. The second statement enables the futures behaviour, causing any asynchronous message send to return a future rather than ''nil''. If ''false'' is passed to the call to ''enableFutures'', only messages marked explicitly as a ''FutureMessage'' will return a future.
 +
 +More information pertaining to the API of the futures language module can be found in the [[:at:tutorial:appendix#futures_and_multifutures|appendix]].
  
 ==== Working with Unresolved Futures ==== ==== Working with Unresolved Futures ====
  
-We have yet to describe what objects can do with futures that are //unresolved//, i.e. what can an object do with a future that does not know its value yet? In many multithreaded languages that introduce the future abstraction, the language dictates that accessing (performing an operation on) an unresolved futures causes the active thread to block; the thread has to wait for the value of the future to be available, before it can carry on. This makes futures a preferred means of //synchronising// two threads with more freedom than is possible using a simple remote procedure call.+We have described a future as a placeholder for the return value of an asynchronous message send which is eventually //resolved// with the expected result. However, we have yet to describe what objects can do with futures that are //unresolved//, i.e. what can an object do with a future that does not know its value yet? In many multithreaded languages that introduce the future abstraction, the language dictates that accessing (performing an operation on) an unresolved futures causes the active thread to block; the thread has to wait for the value of the future to be available, before it can carry on. This makes futures a preferred means of //synchronising// two threads with more freedom than is possible using a simple remote procedure call.
  
-Blocking a thread on a future can be a major source of deadlocks, like any form of blocking, of course. In the actor paradigm where communication between actors should remain strictly asynchronous, this behaviour is obviously unwanted. Furthermore, in a distributed programming context, it is highly undesirable to make one actor block on a future that may have to be resolved by an actor on another machine. It would make the application much more vulnerable because of latency or partial failure, resulting in unresponsive applications at best, and deadlocked applications at worst.+Blocking a thread on a future can be a major source of deadlocks, like any form of blocking. In the actor paradigm where communication between actors should remain strictly asynchronous, this behaviour is obviously unwanted. Furthermore, in a distributed programming context, it is highly undesirable to make one actor block on a future that may have to be resolved by an actor on another machine. It would make the application much more vulnerable because of latency or partial failure, resulting in unresponsive applications at best, and deadlocked applications at worst.
  
-The solution proposed in the [[http://www.erights.org|E language]], and adopted in AmbientTalk, is to disallow direct access to a future by any object. Instead, objects may **only** send **asynchronous** messages to a future object. This enables the future to temporarily buffer such messages until its resolved value is known. The net effect of this solution is that futures actually can be "chained", forming asynchronous //pipelines// of messages.+The solution proposed in the [[http://www.erights.org|E language]], and adopted in AmbientTalk, is to disallow direct access to a future by any object. Instead, objects may **only** send **asynchronous** messages to a future object. This enables the future to temporarily buffer such messages until its resolved value is known. The net effect of this solution is that futures actually can be "chained", forming asynchronous //pipelines// of messages, as will be illustrated in the next section.
  
-<note>Should add example here</note>+As an example of a pipeline of message sends, consider the following code:
  
-When a future eventually becomes resolved with a value, any messages that were accumulated by the future are forwarded asynchronously to the actual return value, such that it appears as if the original object had sent the messages to the actual return value in the first place.+<code> 
 +def booleanFuture := remoteObject<-ask(something); 
 +booleanFuture<-ifTrue:
 +  ... 
 +} ifFalse: { 
 +  ... 
 +
 +</code>
  
-AmbientTalk only allows one method to be synchronously invoked on a future, the ''=='' method. A word of warning though: equality on futures is defined as pointer equality, so a future will only be equal to itselfIt does not compare the parameter object with its actual valueif it would be resolved.+In this example, the message ''ifTrue:ifFalse:'' is sent to a future which will be resolved with a boolean object (the answer to the ''ask'' method)When the ''booleanFuture'' becomes resolved, it will forward the message to its resolved value. The above code shows how you can postpone the execution of code until a future becomes resolved. However, this particular synchronisation technique only works for (futures for) booleans. The following section describes a more general synchronisation technique to await the value of a future for any kind of value.
  
 ==== Working with Resolved Futures ==== ==== Working with Resolved Futures ====
 +
 +When a future eventually becomes resolved with a value, any messages that were accumulated by the future are forwarded asynchronously to the actual return value, such that it appears as if the original object had sent the messages to the actual return value in the first place.
 +
 +<note important>
 +AmbientTalk only allows one method to be synchronously invoked on a future, the ''=='' method. A word of warning though: equality on futures is defined as pointer equality, so a future will only be equal to itself. It does not compare the parameter object with its actual value, if it would be resolved.
 +</note>
  
 As explained above, it is always correct to use asynchronous message sends to communicate with a future. Sometimes, however, we may want to perform some operation on the return value other than message sending, for example, printing it to the screen. If you print the future directly, you get the following: As explained above, it is always correct to use asynchronous message sends to communicate with a future. Sometimes, however, we may want to perform some operation on the return value other than message sending, for example, printing it to the screen. If you print the future directly, you get the following:
Line 271: Line 286:
 </code> </code>
  
-Finally, it is useful to know that ''when:becomes:'' itself returns a future, who will be resolved with the value of applying the observer closure:+Finally, it is useful to know that ''when:becomes:'' itself returns a future, which will be resolved with the value of applying the observer closure: 
  
 <code> <code>
Line 279: Line 294:
 </code> </code>
  
-When the future for ''<-add(1,2)'' becomes resolved with ''sum'', the ''fut'' future will be resolved with the future for the ''<-add(sum,3)'' message. When that message finally returns yet another sum, that sum will become the value of ''fut''.+When the future for ''<-add(1,2)'' becomes resolved with ''sum'', the ''fut'' future will be resolved with the future for the ''<-add(sum,3)'' message. When that message finally returns yet another sum, that sum will become the value of ''fut''. This example also illustrates how futures can be chained by means of the ''when:becomes:'' function forming the so-called asynchrounous pipelines of messages
  
 ==== Futures and Annotated Messages ==== ==== Futures and Annotated Messages ====
Line 304: Line 319:
  
 <code> <code>
 +// to create an explicit future:
 def [future, resolver] := makeFuture(); def [future, resolver] := makeFuture();
-consumer<-give(future); + 
-def val := /* calculate useful value */+// to explicitly resolve a future
 resolver.resolve(val); resolver.resolve(val);
 </code> </code>
Line 313: Line 329:
  
 The resolver also defines a ''ruin(e)'' method which can be used to explicitly ruin a future, causing any attached ''when:becomes:catch:'' blocks to trigger their ''catch:'' clause. The resolver also defines a ''ruin(e)'' method which can be used to explicitly ruin a future, causing any attached ''when:becomes:catch:'' blocks to trigger their ''catch:'' clause.
 +
 +As an example of such conditional synchronization, consider the following example first proposed by Henry Lieberman in his 1987 paper on "Object-oriented Programming in ACT-1". A dating service allows lonely hearts to register their profile allowing it to match people based on their profiles. The dating service can be modelled as an object:
 +
 +<code>
 +def makeDatingService() {
 +  def people := []; // a list of Questionnaire objects
 +  object: {
 +    def match(lonelyHeart) {
 +      // if an ideal mate is found in the list,
 +      //   return its name
 +      // otherwise
 +      //   create a future (a "promise") to answer
 +      //   the lonelyHeart later, when an ideal made
 +      //   has registered with the dating service
 +    }
 +  }
 +}
 +</code>
 +
 +Let us assume that a person is simply identified by a name and its sex:
 +<code>
 +def makePerson(nam, sx) {
 +  object: {
 +    def name := nam;
 +    def sex := sx;
 +  }
 +};
 +</code>
 +
 +The dating service has a little database stored as a simple list. This list does not contain person objects but rather Questionnaire objects. The questionnaire contains the logic necessary to match people. We will assume for the sake of the example that 2 people match if they are of the opposite sex. In addition, a questionnaire object can keep track of an "outstanding answer" (which we model as a first-class future) for a lonely heart that is still waiting for his or her perfect match.
 +
 +<code>
 +def makeQuestionnaire(p) {
 +  def idealPersonResolver;
 +  object: {
 +    def person := p;
 +    def matches(otherQ) { otherQ.person.sex != p.sex };
 +    def wait() {
 +      def [future, resolver] := makeFuture();
 +      idealPersonResolver := resolver;
 +      future
 +    };
 +    def notify(name) { idealPersonResolver.resolve(name) };
 +  };
 +};
 +</code>
 +
 +When a questionnaire is asked to ''wait()'', it returns a future and stores the future's corresponding resolver in a hidden field. Later, when a matching person is found, the future can be explicitly resolved by invoking the questionnaire's ''notify'' method.
 +
 +Armed with these abstractions, we can now fill in the missing logic of the ''match'' method of the dating service:
 +
 +<code>
 +def makeDatingService() {
 +  def people := []; // a list of Questionnaire objects
 +  object: {
 +    def match(lonelyHeart) {
 +      def lonelyHeartQ := makeQuestionnaire(lonelyHeart);
 +      { |return|
 +        people.each: { |idealMateQ|
 +          // an ideal mate was found
 +          if: (idealMateQ.matches(lonelyHeartQ).and: { lonelyHeartQ.matches(idealMateQ) }) then: {
 +            idealMateQ.notify(lonelyHeart.name); // notify idealMate
 +            // remove the person from the database
 +            people := people.filter: { |q| q != idealMateQ };
 +            return(idealMateQ.person.name) // notify lonelyHeart
 +          }
 +        };
 +        // no ideal mate was found, store its questionnaire in the database
 +        people := people + [lonelyHeartQ];
 +        lonelyHeartQ.wait(); // return a future for the ideal person's name
 +      }.escape();
 +    }
 +  }
 +}
 +</code>
 +
 +Below, we define an auxiliary method that illustrates how a lonely heart has to interact with the dating service.
 +
 +<code>
 +def d := makeDatingService();
 +def register(p) {
 +  when: d<-match(p)@FutureMessage becomes: { |name|
 +    system.println(p.name + " matched with " + name);
 +  };
 +};
 +</code>
 +
 +The key to our conditional synchronization is that the ''when:becomes:'' listener will only be invoked if and when a matching person has registered itself with the dating service. As long as this condition has not been fulfilled, the future will remain unresolved. In fact, the future returned by ''d<-match(p)'' has been resolved with another future, the one returned by ''Questionnaire.wait()''. However, recall that a future resolved by another unresolved future does not really count as being "resolved" and will only trigger its listeners once its "dependent" future has been resolved.
 +
 +The complete source code of the above example can be found in the file ''at/demo/DatingService.at'' found in the distribution. The directory ''at/demo'' also contains an example solution to Dijkstra's famous "dining philosophers" problem, which makes use of a similar technique to achieve conditional synchronization of the philosophers.
  
 ===== Actor Mirrors ===== ===== Actor Mirrors =====
Line 318: Line 424:
 An actor in AmbientTalk is primarily a //host// for regular objects. It is equipped with a message queue to receive asynchronous messages sent to one of its objects. The mirrors on these objects have corresponding meta-level operations such as ''send'' and ''receive'' that can be overridden to customise e.g. message sending on a per-object basis. An actor in AmbientTalk is primarily a //host// for regular objects. It is equipped with a message queue to receive asynchronous messages sent to one of its objects. The mirrors on these objects have corresponding meta-level operations such as ''send'' and ''receive'' that can be overridden to customise e.g. message sending on a per-object basis.
  
-Some operations, such as creating and sending asynchronous messages are useful to reify at the //actor level//. With such a reification, the programmer could override the way messages are sent by any object from within an actor. The mirror on an actor that reifies these operations is bound to the ''actor'' variable, defined at top-level. Evaluating ''actor'' returns the mirror on the current actor. It defines operations such as ''createMessage'' and ''send'' which can be explicitly invoked, or even overridden by the programmer.+Some operations, such as creating and sending asynchronous messages are useful to reify at the //actor level//. With such a reification, the programmer could override the way messages are sent by any object from within an actor. The mirror on an actor that reifies these operations can be acquired by invoking the function ''reflectOnActor()'', defined at top-level. Evaluating ''reflectOnActor'' returns the mirror on the current actor. It defines operations such as ''createMessage'' and ''send'' which can be explicitly invoked, or even overridden by the programmer.
  
-Overriding the actor's metaobject protocol can be done by //installing// a new protocol object. This is done by invoking the ''install:'' method on ''actor''. The following code installs a new MOP that logs any messages sent by any object in the current actor:+Overriding the actor's metaobject protocol can be done by //installing// a new protocol object. This is done by invoking the ''becomeMirroredBy:'' method on the actor's mirror. The following code installs a new MOP that logs any messages sent by any object in the current actor:
  
 <code> <code>
-def oldmirror := actor.install: (extend: actor with: {+def actor := reflectOnActor(); 
 +def oldmirror := actor.becomeMirroredBy: (extend: actor with: {
   def send(msg) {   def send(msg) {
     log(msg);     log(msg);
Line 331: Line 438:
 </code> </code>
  
-Notice that, in this example, the new metaobject protocol is an extension of the old protocol. This enables it to invoke its parent's behaviour simply by means of a super-send. Note also that the ''install:'' primitive returns the previously installed mirror. This is useful for when an actor mirror should only temporarily be installed. The old mirror can later be re-installed.+Notice that, in this example, the new metaobject protocol is an extension of the old protocol. This enables it to invoke its parent's behaviour simply by means of a super-send. Note also that the ''becomeMirroredBy:'' primitive returns the previously installed mirror. This is useful for when an actor mirror should only temporarily be installed. The old mirror can later be re-installed.
  
 +<note>
 For a good use case of actor mirrors, see the ''at/lang/futures.at'' file in the system library. This file implements future-type message passing. It uses a custom actor mirror that overrides ''createMessage'' -- which is invoked whenever an asynchronous message is created -- to attach a future to the message. For a good use case of actor mirrors, see the ''at/lang/futures.at'' file in the system library. This file implements future-type message passing. It uses a custom actor mirror that overrides ''createMessage'' -- which is invoked whenever an asynchronous message is created -- to attach a future to the message.
 +</note>
  
 Other methods that can be overridden are ''require'' and ''provide'', which reify the export and discovery of objects, and ''createMirror'', which is invoked by the ''reflect:'' primitive. By overriding this //factory method//, it becomes possible to easily customize the behaviour of mirrors defined on local objects. Other methods that can be overridden are ''require'' and ''provide'', which reify the export and discovery of objects, and ''createMirror'', which is invoked by the ''reflect:'' primitive. By overriding this //factory method//, it becomes possible to easily customize the behaviour of mirrors defined on local objects.
at/tutorial/actors.txt · Last modified: 2020/02/05 21:26 by elisag