2011-08-29: Fibonacci Program using an Actor

In this post, I rewrite my Swing-based fibonacci program using the Fantom Widget Toolkit and an Actor. Fantom uses Actors to manage concurrency. There are a number of conceptual challenges when starting to use Actors, and some of this is a consequence of the requirement for Actors and the objects they manipulate to be immutable. For an Actor to be immutable, it must not contain any instance variables which are mutable. Hence, Actors cannot themselves store mutable state.

Instead of instance variables, a special Map, called Actor.locals, is used to store mutable objects. However, Actor.locals cannot be thought of as an equivalent to instance variables for the current Actor: Actor.locals are local to a particular thread.

Actors are triggered into action on receipt of a message. When sent a message, the runtime generates a thread for the Actor to do its work - thus, the Actor will not necessarily be running in the same thread for every message. This is why Actor.locals, which is tied to a thread, cannot be used to store intermediate results between messages.

The only thread (in an FWT application) which is guaranteed to be working and retrievable is the GUI event thread. The method Desktop.callAsync is used to call a provided function on the GUI event thread.

We can use an Actor to implement the equivalent of SwingWorker by sending the Actor a message to trigger it to do its work; rather like the doInBackground method of a SwingWorker which is started on calling execute. The Actor can then, when finished, call a method in the GUI event thread, rather like the done method of a SwingWorker is invoked on the event thread.

FWT Fibonacci Application

01: using concurrent
02: using fwt
04: class Calculate
05: {
06:   native static Int fib (Int n)
07: }
09: const class Worker : Actor
10: {
11:   // in constructor, place widgets we need to access in GUI update
12:   // within Actor.locals
13:   new make (Label result, Button button) : super (ActorPool ())
14:   {
15:     Actor.locals["result"] = result
16:     Actor.locals["button"] = button
17:   }
19:   // receive any messages sent to this worker
20:   // Note: Actor.locals *not* accessible here!
21:   // so any information for calculation must be included with message
22:   override Obj? receive (Obj? msg)
23:   {
24:     // we only care about one message - a number to compute from
25:     n := msg as Int
26:     if (n != null)
27:     {
28:       // do the actual computation
29:       startTime := DateTime.now
30:       res := Calculate.fib (n)
31:       // call the 'update' method on the GUI thread
32:       Desktop.callAsync |->| { update(n, res, DateTime.now - startTime) }
33:     }
34:     return null
35:   }
37:   // this method called in GUI thread to update GUI widgets
38:   // note: Actor.locals from constructor is accessible here
39:   Void update (Int n, Int res, Duration time)
40:   {
41:     // retrieve result from Actor.locals, and update it
42:     result := Actor.locals["result"] as Label
43:     result?.with // use safe invoke, just in case 'result' is null
44:     {
45:       text = "Fib($n) = $res in $time"
46:       pack
47:     }
48:     // retrieve button from Actor.locals, and update it
49:     button := Actor.locals["button"] as Button
50:     button?.with
51:     {
52:       text = "compute"
53:       enabled = true
54:       pack
55:     }
56:   }
57: }
59: class Main
60: {
61:   static Void main ()
62:   {
63:     chooseN := Text { text = "1" }
64:     result := Label ()
65:     button := Button { text = "compute" }
66:     button.onAction.add |Event e|
67:     { // when button clicked
68:       button.with
69:       {
70:         text = "Busy"
71:         enabled = false
72:       }
73:       Worker(result, button).send (chooseN.text.toInt)
74:     }
76:     Window
77:     {
78:       title = "Fibonacci"
79:       GridPane // place widgets in a column
80:       {
81:         chooseN,
82:         button,
83:         result,
84:       },
85:     }.open
86:   }
87: }

In line 73 of the code, we create the Worker object, passing it references to the results label and button; these are stored in the Actor.locals map in lines 15 and 16.

In line 73, we also send a message to the Worker: this message is just a number. We receive that message in the 'receive' method at line 22. This method is responsible for responding to all messages that the Worker may receive, and so it must first check that the message is a number; this is done in lines 25 and 26.

Lines 29 and 30 perform the computation, and this is in a background thread, so does not interfere with the GUI event loop.

Line 32 then calls the 'update' method on the GUI event loop, so the graphical displays can be updated with the result.

Line 42 retrieves the 'result' widget from the Actor.locals store, and lines 43 onwards perform the update on that widget. As it is possible the 'result' widget is not in the store, we should take care in case 'result' has been set to 'null'; in line 43 we use the safe invoke operator '?.', which will only call the succeeding method if the instance is not null.

When first trying to understand Actors, it is easy to get confused about what can be retrieved from Actor.locals and when. In this program, the widget values in the Actor.locals map are only retrievable when the Worker is running in the GUI thread. If you try a check for Actor.locals["result"] in the 'receive' method (say at line 24), you will get the result 'null'.

Comparison with Swing version

When put side-by-side, there is not much difference between the Worker class implemented using an Actor and the Worker class using SwingWorker. In both cases, the constructor is passed the GUI widgets, and in both cases these widgets are updated in a separate method guaranteed to be run in the GUI thread. The Actor implementation means the stored widgets must be retrieved from Actor locals, but the null-safe invocation of the with-clause means there is no need to check if the widget has been successfully retrieved.

Page from Peter's Scrapbook, output from a VimWiki on 2024-01-29.