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.
01: using concurrent 02: using fwt 03: 04: class Calculate 05: { 06: native static Int fib (Int n) 07: } 08: 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: } 18: 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: } 36: 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: } 58: 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: } 75: 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: } 88: 89:
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'.
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.