Agents of Swing

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!

9 thoughts on “Agents of Swing

  1. Jar Fuay

    I’d like to thank you for this wonderful, wonderful tutorial. Before this, I didn’t get Clojure agents that well—now I totally get why they’re useful, with an example of how to do Swing in Clojure to boot! Thank you very very much!

  2. Michael Wood

    This is great :) Thanks.

    I’ve noticed that the agent seems to carry on running if you just close the window without first clicking on Stop. So if you run it once and close the window while it’s running, then do it again, the second instance will be slower. If you keep on doing this, Java uses more and more CPU and the running instance of the app gets slower and slower.

    How would one arrange for (send flipper stop) to be called when the window is closed?

    Also, if you run multiple instances of the app at the same time, they get really slow too. I suppose you wouldn’t normally run two instances of something like this at the same time, but there might be two windows doing something different from each other that you want to be able to run concurrently. Any thoughts on this?

  3. Stuart Post author

    Michael- you’re right, each window creates a new flipper, which will eventually use up all your CPU resources.

    One solution is to assign the flipper to a global Var, so that all the windows connect to the same flipper.

    Another way is to automatically stop the flipper when the window is closed. This is easy enough. We need to add a WindowListener on the JFrame. Insert the following lines just above (.pack) in the flipper-app function:

    (.addWindowListener 
     (proxy [java.awt.event.WindowListener] []
       (windowActivated [e])
       (windowClosed [e])
       (windowClosing [e]
                      (send flipper stop)
                      (.stop timer))
       (windowDeactivated [e])
       (windowDeiconified [e])
       (windowIconified [e])
       (windowOpened [e])))

    As to your second question, running multiple instances of any CPU-intensive process will be slow.

    In fact, Clojure puts more strain on the system because it makes full use of all your CPU cores. That’s good if you want your computation to finish faster, not so good if you want to leave some CPU for other applications.

    Clojure doesn’t provide a way to explicitly control how many cores it uses, but it could someday. Some operating systems also allow you to restrict the number of cores a process can use. As multi-threaded programming becomes more prevalent, CPU resource management should be more accessible.

  4. Michael Wood

    Thanks :) That WindowListener was what I was looking for.

    Yes, of course running multiple CPU intensive things at the same time will slow the machine down. What I meant was that it seemed not to be updating the display at 0.1s intervals anymore, but I suppose it could also be that the values were taking longer than 0.1s to update and if so, I wouldn’t be able to tell the difference :) I suppose part of my problem is that I have a single core machine.

  5. Jeff Schwab

    Thank you for the excellent series of posts. Would it make more sense to set the text-boxes’ “editable” property, rather than “enabled”? Also, would there be any disadvantage to using WindowAdapter, instead of WindowListener, so as to reduce boilerplate?

  6. Nic

    Great blog. I found it very useful.
    The one macro confused me a little. The event parameter in with-action seems to have
    no role. This definition of the macro is clearer for me. Is it also valid or does it have issues I am not seeing ?

    (defmacro with-action [component & body]
    `(. ~component addActionListener
    (proxy [java.awt.event.ActionListener] []
    (actionPerformed [~’event] ~@body))))

  7. Jim

    Thanks a lot Stuart…I really like your macros! Just a small remark however…Your with-action macro will create a brand new ActionListener for each component it’s called with. Is this desirable in general? What if you have 50 buttons? The way i usually do it in Java is using a single ActionListener and by identifying the source of the event from the action command. Isn’t this more efficient in general? Now in all fairness, i tried to think of a way to do exactly that and I faced problems because there is no concept of class in Clojure (unless you get down and dirty with :gen-class) so I don’t know how to create an ActionListerner that just pass it around as “this” as i would do in Java. Any clarification/help is greatly appreciated…i just want to know if creating separate listeners for single actions is legit…

  8. Pingback: String Matcher | Vinnie Monaco – Research in Software

Comments are closed.