The title of this post would make a good name for a band.
Anyway, today I’m going to talk about Swing and concurrency and Clojure.
The Swing framework is not thread-safe. That may sound strange at first, but there’s actually some sound technical reasoning behind it.
Basically, the Swing designers realized that, in order to have a multi-threaded GUI, you need locks everywhere, which typically means you have bugs everywhere.
That’s not to say a Swing application can’t be multi-threaded. Swing merely requires that the GUI be single-threaded. In Swing, all GUI-related code must run on a special thread called the event dispatch thread, which is automatically created by the Swing framework.
By design, event listeners such as ActionListener.actionPerformed always run on the event dispatch thread. To run arbitrary code on the event dispatch thread, you can use the SwingUtilities.invokeLater method.
As was pointed out in a comment on my last post, this is how all of my example apps should be started. The invokeLater method takes a Runnable argument. In Clojure, this is easy, because all Clojure functions are Runnable. So, for example, the temp-app program should be started like this:
(SwingUtilities/invokeLater temp-app)
As a consequence of running all GUI code on a single thread, any GUI code that blocks can “freeze” the entire application. Event listeners, therefore, must execute quickly and delegate long operations like I/O to other threads.
Java 6 provides the SwingWorker class to manage long-running background tasks. Unfortunately, the implementation of SwingWorker is heavily dependent on protected methods and concrete inheritance, features that are awkward to use in Clojure.
But wait, wasn’t Clojure designed for concurrency? Yes it was, and we can replace SwingWorker with Clojure’s more-powerful tools.
In this post, I will recreate the Flipper application from the Swing tutorials.
* * *
The point of Flipper is to test the “fairness” of Java’s random number generator. It uses java.util.Random to generate random booleans, simulating a series of coin flips. The number of “heads” (boolean true) should converge to half of the total number of flips.
The Java version of Flipper (source here) does the coin flips in a SwingWorker thread, which sends intermediate results to a GUI.
Our version will use Clojure Agents, a much more flexible and powerful alternative.
Lets dig into some code. First, we need a function to create a “Flipper” agent:
(defn new-flipper []
(agent {:total 0, :heads 0,
:running false,
:random (java.util.Random.)}))
This sets up the initial state of the agent, with its own random number generator, two counters, and a boolean flag to designate whether or not the flipper is running.
The flipper agent will have three actions. An agent action is just a function taking one argument, which is the current state of the agent. Whatever the function returns becomes the new state of the agent.
Additionally, in the body of the action, the special Var *agent* is bound to the agent itself. The action function can send other actions to *agent*; those actions will not run until after the current action completes.
Our flipper’s first action is calculate:
(defn calculate [state]
(if (:running state)
(do (send *agent* calculate)
(assoc state
:total (inc (:total state))
:heads (if (.nextBoolean (:random state))
(inc (:heads state))
(:heads state))))
state))
If the flipper is currently “running,” calculate will call the random number generator and update the counters. It will also send itself to the agent again, creating a loop.
If the flipper is not “running,” calculate does nothing, but it must still return the original state.
The remaining two actions control the “running” state of the agent:
(defn start [state]
(send *agent* calculate)
(assoc state :running true))
(defn stop [state]
(assoc state :running false))
These actions just change the value of :running in the agent’s state. The start action also sends the calculate action to kickstart the calculation loop.
To watch the agent in action, try this at the REPL:
(def flipper (new-flipper))
(send flipper start)
The flipper is now running in the background. At any time, you can retrieve the current state of the flipper with (deref flipper), or simply @flipper. Try it a few times and see how the counters change, as in this sample REPL session:
user> @flipper
{:total 365067, :heads 182253, :running true, ...
user> @flipper
{:total 679283, :heads 338549, :running true, ...
user> @flipper
{:total 1030204, :heads 513767, :running true, ...
To stop the calculation, type
(send flipper stop)
Subsequent calls to @flipper will all show the same value. You can restart the calculation by typing (send flipper start) again.
We need one more function to make our flipper complete. We want to compute the “unfairness” of the random number generator, or how far it is from 0.5. This is easy:
(defn error [state]
(if (zero? (:total state)) 0.0
(- (/ (double (:heads state))
(:total state))
0.5)))
The if expression is just there to avoid divide-by-zero errors in the initial case.
Note that, although I used the same argument name, state, as in the actions, the error function is not meant to be called as an action with send. Instead, we will call it on the current state of the agent like this:
(error @flipper)
* * *
The nice thing about this flipper is that, unlike the Java version, it is completely decoupled from the GUI. We can design and test it at the REPL before writing any GUI code at all.
(The Java version could have been decoupled. But because Swing requires you to create so many classes anyway, the temptation is always there to combine GUI code with “process” code.)
So let’s get this GUI going. Unlike the Java example, which used callbacks from the flipper to update the GUI, our GUI will update at a fixed rate of 10 times per second. To do this we will use a javax.swing.Timer, which fires an event every n milliseconds.
First, some imports and a helper function to create the text fields with common attributes:
(import '(javax.swing JPanel JFrame JButton JTextField
JLabel Timer SwingUtilities))
(defn text-field [value]
(doto (JTextField. value 15)
(.setEnabled false)
(.setHorizontalAlignment JTextField/RIGHT)))
Next, we will reuse the on-action macro from an earlier post, except I’ve renamed it with-action because my editor’s automatic indentation looks better that way.
(defmacro with-action [component event & body]
`(. ~component addActionListener
(proxy [java.awt.event.ActionListener] []
(actionPerformed [~event] ~@body))))
And now we’re ready for the app itself:
(defn flipper-app []
;; Construct components:
(let [flipper (new-flipper)
b-start (JButton. "Start")
b-stop (doto (JButton. "Stop")
(.setEnabled false))
total (text-field "0")
heads (text-field "0")
t-error (text-field "0.0")
timer (Timer. 100 nil)]
;; Setup actions:
(with-action timer e
(let [state @flipper]
(.setText total (str (:total state)))
(.setText heads (str (:heads state)))
(.setText t-error (format "%.10g" (error state)))))
(with-action b-start e
(send flipper start)
(.setEnabled b-stop true)
(.setEnabled b-start false)
(.start timer))
(with-action b-stop e
(send flipper stop)
(.setEnabled b-stop false)
(.setEnabled b-start true)
(.stop timer))
;; Create window and layout:
(doto (JFrame. "Flipper")
(.setContentPane
(doto (JPanel.)
(.add (JLabel. "Total:"))
(.add total)
(.add (JLabel. "Heads:"))
(.add heads)
(.add (JLabel. "Error:"))
(.add t-error)
(.add b-start)
(.add b-stop)))
(.pack)
(.setVisible true))))
The GUI has three text fields showing the total number of flips, the number of “heads,” and the error rate. The “Start” button starts the flipper; the “Stop” button suspends it. You can start and stop as many times as you like without losing the results of the calculation (another improvement over the Java version, which restarts at zero each time).
The important code is in the with-action bodies. (Don’t confuse Swing ActionListeners with Agent actions.) Remember, event listeners must execute quickly, so all they do is update the visible parts of the GUI.
The “Start” button kicks off the flipper and activates the Timer. The Timer fires every 100 milliseconds, updating the text fields from the current state of the flipper. The “Stop” button suspends both the Timer and the flipper.
Run the application like this:
(SwingUtilities/invokeLater flipper-app)
We didn’t do any fancy layout because it wasn’t necessary for the example. But if you want something prettier, here’s another version using the GridBagLayout macros from my last post:
(import '(java.awt GridBagLayout Insets))
(defmacro set-grid! [constraints field value]
`(set! (. ~constraints ~(symbol (name field)))
~(if (keyword? value)
`(. java.awt.GridBagConstraints
~(symbol (name value)))
value)))
(defmacro grid-bag-layout [container & body]
(let [c (gensym "c")
cntr (gensym "cntr")]
`(let [~c (new java.awt.GridBagConstraints)
~cntr ~container]
~@(loop [result '() body body]
(if (empty? body)
(reverse result)
(let [expr (first body)]
(if (keyword? expr)
(recur (cons `(set-grid! ~c ~expr
~(second body))
result)
(next (next body)))
(recur (cons `(.add ~cntr ~expr ~c)
result)
(next body)))))))))
(defn flipper-app2 []
;; Construct components:
(let [flipper (new-flipper)
b-start (JButton. "Start")
b-stop (doto (JButton. "Stop")
(.setEnabled false))
total (text-field "0")
heads (text-field "0")
t-error (text-field "0.0")
timer (Timer. 100 nil)]
;; Setup actions:
(with-action timer e
(let [state @flipper]
(.setText total (str (:total state)))
(.setText heads (str (:heads state)))
(.setText t-error (format "%.10g" (error state)))))
(with-action b-start e
(send flipper start)
(.setEnabled b-stop true)
(.setEnabled b-start false)
(.start timer))
(with-action b-stop e
(send flipper stop)
(.setEnabled b-stop false)
(.setEnabled b-start true)
(.stop timer))
;; Create window and layout:
(doto (JFrame. "Flipper")
(.setContentPane
(doto (JPanel. (GridBagLayout.))
(grid-bag-layout
:insets (Insets. 5 5 5 5)
:gridx 0, :anchor :LINE_END
:gridy 0, (JLabel. "Total:")
:gridy 1, (JLabel. "Heads:")
:gridy 2, (JLabel. "Error:")
:gridx 1, :anchor :LINE_START
:gridy 0, total
:gridy 1, heads
:gridy 2, t-error
:gridx 0, :gridy 3, :gridwidth 2, :anchor :CENTER
(doto (JPanel.)
(.add b-start)
(.add b-stop)))))
(.pack)
(.setVisible true))))
I centered the buttons below the text fields by embedding them in a nested JPanel that spans the full width of the window.
Run this example as:
(SwingUtilities/invokeLater flipper-app2)
Enjoy!