CS 3, Section 1
MiniLecture - May 7, 2001

Hello and welcome to today's CS 3 minilecture. Whether you are officially assigned to this section or otherwise, you are free to attend whichever sections fit your schedule. We hope you find this section informative enough to continue attending.

Personal information

Section 1b : Stephen Bird | bird@ugcs.caltech.edu | Lloyd 223 | x1694

Read the Lab

As always, we'll dive into the material quickly. Read the lab 5 documentation before reading this minilecture.

Threads

Threads are the basic encapsulations of processes.

Having several threads in a single program is a little like loading emacs or netscape into the backgroup by appending '&' to their call. However, unlike different programs running from the same shell, threads within a process can talk to each other.

By default, Java gives you one thread of execution. This thread starts by executing the main function. When execution of the main function is finished, this thread is then used to handle GUI events if any GUI exists.

The lab documentation does a great job of explaining threads by suggesting the difference between Evil Count and Good Count. Lets take a look at the threads of execution for each program and how it reacts to user input.

Evil Count Execution
Main Thread Code Thread Status
public static void main(String[] args)
{
  new EvilCount();
}


The main thread begins by calling the constructor new EvilCount();
No GUI has been yet created for possible user input.
public EvilCount()
{
  super("Evil");                      // set title
  Container c = getContentPane();     // get content pane
  c.setLayout(new GridLayout(1, 1));  // set content layout: 1x1 grid
  c.add(new MyButton());              // create a button

  // handle window close events:
  addWindowListener(new WindowAdapter()
  {
    public void windowClosing(WindowEvent e) { System.exit(0); }
  } );

  pack();                             // set window size
  show();                             // show window
}















Within the constructor, the main thread creates a GUI and eventually sets it
visible. The program should now become interactable.
wait
Now that a GUI is available, the main thread waits in the Java virtual machine
to be dispatched to GUI events. Consider if the new MyButton were
pressed by the user.
public void actionPerformed(ActionEvent e)
{
  long start = System.currentTimeMillis();  // remember when we started

  for(int i = 1; i <= 999999999; i++)       // count to 999999999
    System.out.println(i);

    // print elapsed time, in milliseconds:
  System.out.println("time: " + (System.currentTimeMillis()-start) + " ms");
}





After the button click, the main thread is dispatched to handle the button
press. After calling actionPerformed the thread is caught in a
lengthy for loop (remember that for loops without braces repeated execute the
one command immediately following the for loop).

After becoming busy within this GUI event for loop, the program looses its GUI thread to handle GUI events. Closing the window or repressing the button accomplishes nothing. This can be solved by using threads.

Good Count Execution
Main Thread Code Created Thread Code Thread Status
The same setup occurs to create the window and button. The difference occurs when the user clicks the created MyButton.
public void actionPerformed(ActionEvent e){
  long start = System.currentTimeMillis();  // remember when we started
  new Counter(999999999);                   // count to 999999999
        
  // print elapsed time, in milliseconds:
  System.out.println("time: " + (System.currentTimeMillis()-start) + " ms");
}


The import change is the call to the Counter constructor instead
of entering a long for loop.
public Counter(int num){
  total = num;                // remember the count
  setPriority(MIN_PRIORITY); // set low priority
  start();                    // start this new thread
}



The thread initializes its total and priority. It then calls start() upon
itself. start() is a call to the Java virtual machine which creates a new
thread and starts this thread at your provide run() method.
wait
public void run(){
  for(int i = 1; i <= total; i++)
    System.out.println(i);
}
The main thread makes the second call to System.currentTimeMillis() within the
actionPerformed method. The main thread then returns to the Java virtual
machine to be dispatched to the next GUI events. Meanwhile and independently,
the separately created Counter thread chugs away at its for loop.

Problem Specification

As hinted above, you will need to make use of threads to ensure your program does not hang as Evil Count does. Whereas Evil Count has a single for loop that keeps the GUI thread busy, lab 5 requires a great deal of computation.

Our problem is not to compute the mandlebrot set, but instead to create a gradient of color dependent upon the radius from the origin. Like lab 5, we also will allow zooming and reseting of the bounds.

Simple Solution

The simplest solution is similar to Evil Count in that it uses only one thread and thus the GUI will 'hang' while the picture is being drawn. Nevertheless, this solution will introduce a few new concepts you should become familiar with. It will also be illuminating to see how the addition of multi-threading to a program affects the design.

Consider the private data members of this class. My way of thinking led to using the Rectangle2D.Double class to represent the complex boundaries of the drawn picture. (bounds.x, bounds.y) represents the complex point in the upper left part of the picture. For your mandlebrot set, you'll want to start (before zooming) displaying between [-2, 2] in x and [-2, 2] in y. The defaultBounds is thus set to (-2, 2) for the upper left point with width and height both 4.

The added mouse listener may display some new concepts. For my solution, we allow the user to zoom by pressing the left mouse button and switch back to the default bounds by pressing the right mouse button. This was accomplished after skimming the online tutorial on writing mouse listeners.

  defaultBounds = new Rectangle2D.Double(-2, 2, 4, 4);

  ...

  addMouseListener( new MouseAdapter() {
    public void mousePressed (MouseEvent e) {
      if ((e.getModifiers() & InputEvent.BUTTON1_MASK)
            == InputEvent.BUTTON1_MASK) {

	double newWidth = bounds.width / 2;
	double newHeight = bounds.height / 2;
	bounds = new Rectangle2D.Double(xPixelToReal(e.getX())
	                                - (newWidth / 2),
					yPixelToImag(e.getY())
					+ (newHeight / 2),
					newWidth, newHeight);
	repaint();
      }
      else if ((e.getModifiers() & InputEvent.BUTTON3_MASK)
		 == InputEvent.BUTTON3_MASK) {
        bounds = defaultBounds;
	repaint();
      }
    }
  } );

Determining which mouse key was pressed occurs within the logic of your mouseXXXed code. In this case, we have overloaded mousePressed. Testing which button was pressed requires getting the MouseEvent object and calling its method getModifiers(). This result is then tested against the button one mask and the button three mask.

Once again, you see the need for a conversion method from pixel to complex. You may discover you need the reverse as well for this lab. Luckily, you can specify how you wish to describe your complex and pixel boundaries. With our current implementation, the conversion is quite simple. Don't forget to subtract in the case of yPixelToImag.

  private double xPixelToReal (int x) {
    //   left Z bound + (pixel distance from x converted into real)
    return bounds.x + (((double) x) / pixels.width) * bounds.width;
  }

  private double yPixelToImag (int y) {
    //   top Z bound - (pixel distance from y converted into imaginary)
    return bounds.y - (((double) y) / pixels.height) * bounds.height;
  }

As you can see, the ease of conversion warrants our use of Rectangle2D.Double and Dimension objects.

After the bounds are adjusted depending upon the mouse button clicked, a repaint is requested. This moves our thread into the paint(Graphics g) method.

  public void paint (Graphics g) {
    Graphics2D g2 = (Graphics2D) getGraphics();
    pixels = getSize();
    double real, imag, hue;

    for(int x = 0; x < pixels.width; ++x) {

      real = xPixelToReal(x);

      for(int y = 0; y < pixels.height; ++y) {

        imag = yPixelToImag(y);
	hue = ((real * real) + (imag * imag));
	switch(hue) {
          case 0: g2.setPaint(Color.black); break;
	  case 1: g2.setPaint(Color.red); break;
	  ...
	  case ?: g2.setPaint(Color.blue); break;
	  case MAX: g2.setPaint(Color.white); break;
	  default: g2.setPaint(Color.white); break;
	}

        g2.draw( new Rectangle2D.Double(x, y, 1, 1) );
      }
    }
  }

The above calculates a rought hue of the color expected and performes a switch estimatation. If we want to implement eight colors, we must provide eight switch statements. If we later want to increase the number of colors used, you must then add more switch cases and recompile. Instead, it be better to use a dynamic spectrum. That is to say calculate the percentage between red and blue.

This can be accomplished by making a call to the static function Color.getHSBColor(float hue, float saturation, float brightness).

  private static int maxEval = 10;

  ...

  public void paint (Graphics g) {
    Graphics2D g2 = (Graphics2D) getGraphics();
    pixels = getSize();
    double real, imag, hue;

    for(int x = 0; x < pixels.width; ++x) {

      real = xPixelToReal(x);

      for(int y = 0; y < pixels.height; ++y) {

        imag = yPixelToImag(y);
	hue = ((real * real) + (imag * imag)) / maxEval;

	g2.setPaint(Color.getHSBColor( (float) hue, 1, 1 ));
	g2.draw( new Rectangle2D.Double(x, y, 1, 1) );
      }
    }
  }

This code provides us with a dymanic spectrum which can be adjusted during program execution by adjusting the data member maxEval.

Unfortunately, within this code we have two large for loops and some taxing calculations. This code effectively 'hangs' our program by trapping the thread meant to respond to GUI events in excessive computation.

Threaded Solution

The solution is to use a thread respond to GUI events as well as a thread to compute and paint. The GUI event thread will create this compute/paint thread whenever paint(Graphics g) is called becuase the panel needs to be repainted.

To accomplish this, we must provide a class that can be run by a thread. This class will compute the appropriate color and paint each pixel. To be run within a thread, we implement Runnable, which consists of providing a run() method to perform our needed computation and painting.

  private class GradientPaintRun implements Runnable
  {
    public void run () {
      Graphics2D g2 = (Graphics2D) getGraphics();
      pixels = getSize();
      double real, imag, hue;

      for(int x = 0; x < pixels.width; ++x) {

        real = xPixelToReal(x);

	for(int y = 0; y < pixels.height; ++y) {

	  imag = yPixelToImag(y);
	  hue = ((real * real) + (imag * imag)) / maxEval;

	  g2.setPaint(Color.getHSBColor( (float) hue, 1, 1 ));
	  g2.draw( new Rectangle2D.Double(x, y, 1, 1) );
		
	}
      }
    }
  }

As you can see, we've simply moved our paint(Graphics g) into a class's run() method. What remains is to create the thread and begin its processing everytime the panel needs to be repainted. This is performed inside our old JPanel.paint(Graphics g). We first create a new thread, passing in an instance of our Runnable class to the Thread constructor. We then ask the Java virtual machine to create the new thread and call its run method.

With this in mind, the code to accomplish this follows.

  public void paint (Graphics g) {
    paintRun = new GradientPaintRun();
    new Thread(paintRun).start();
  }

As you can see, it is both simple and elegant. Everytime the panel must be repainted, a new thread is created for the calculations. When the calculations are done and the panel is drawn, the calculation/paint thread returns from the method run() and the Java virtual machine destroys the created system level thread. You needn't worry about cleaning up the thread yourself.

Unfortunately, the calculation/paint thread can become invalid. This occurs while the calculation/paint thread is busy at work and the GUI event thread catches a mouse button press. The GUI event thread changes the bounds appropriately and calls repaint() which indirectly calls paint(Graphics g) and creates another calculation/paint thread. It is very possible the old calculation/paint thread has not finished the code in its run() method and therefore continues to calculate and paint. Thus, two separate threads will be attempting to paint to the same panel.

Try compiling and running the solution to see what happens. Most often, the old calculation/paint thread is far enough into the for loops that its paint is eventually overwritten by the newly created calculation/paint thread. Still it is an occurence we would like to avoid. After all, we are wasting processor power with the calculations being performed by the old calculation/paint thread. The solution is to provide a means of stopping our Runnable within its lenghty run() method.

Stopable Threaded Solution

As mentioned above, we simply need to provide a solution to get a thread to return from run(). The class now contains a private boolean value done indicatin whether it should be done. The value can be set to true by calling the finish() method. To ensure run() returns soon after finish() is called, we make both for loops finish if done ever becomes true.

  private class GradientPaintRun implements Runnable
  {

    private boolean done;

    public void finish () { done = true; }

    public void run () {
      Graphics2D g2 = (Graphics2D) getGraphics();
      pixels = getSize();
      double real, imag, hue;

      for(int x = 0; x < pixels.width && !done; ++x) {

        real = xPixelToReal(x);

	for(int y = 0; y < pixels.height && !done; ++y) {

	  imag = yPixelToImag(y);
	  hue = ((real * real) + (imag * imag)) / maxEval;

	  g2.setPaint(Color.getHSBColor( (float) hue, 1, 1 ));
	  g2.draw( new Rectangle2D.Double(x, y, 1, 1) );
	}
      }
    }
  }

Now whenever paint(Graphics g) occurs (indicating the need for a repaint), we ensure the old thread (if it exists) finishes soon after creating the new calculation/paint thread as before.

  public void paint (Graphics g) {
    if (paintRun != null) {
      paintRun.finish();
    }

    paintRun = new GradientPaintRun();
    new Thread(paintRun).start();
  }

It is still possible the thread will overlap while the old calculation/paint thread is exiting the two for loops, but far less likely that any overlap will be noticable by the user.