Computer Science Honours 2011

Distributed & Parallel Processing
Practical One Solution

Use the executor mechanisms in java.util.concurrent to parallelise the Mandelbrot program.

The solution is shown below, with the changes to the original Mandelbrot program highlighted.

The biggest issue is keeping track of the "future" results so that they can be retrieved once all the tasks have been initiated.  I used a simple LinkedList for this.  The interesting part of the program lies in the generateImage method, which now also incorporates the functionality of the waitForResults method.  Note that the results are simply retrieved in the same order as the tasks were created.  This is not necessarily the most efficient approach if the tasks are not well-balanced — a more sophisticated approach would be to check the status of the future, and, if it is not yet ready, return it to the end of the list.

Note too that the results are "complex" in that they consist of an array of pixel values and the starting y coordinate of the block.  This requires a simple class, Result, to encapsulate the result returned by the call method (defined at the very bottom of the class).

I used an executor service with a fixed thread pool with four threads, aiming to make optimal use of the four-core processor available.

/* Mandelbrot program, modified to use java.util.concurrent
 * executor services.
 * George Wells  --  30 August 2009
 */

import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Date;
import java.util.concurrent.*;
import java.util.LinkedList;

public class MandelbrotConc extends Applet
    implements Runnable, MouseListener, MouseMotionListener
  { private static final int NUM_THREADS = 4; // Use four threads for 4-core CPU
    private int xsize;      // dimensions of window
    private int ysize;
    private int taskSize;
    private final int NUM_TASKS = 5;
    private Thread master;
    private ExecutorService exec = Executors.newFixedThreadPool(NUM_THREADS); // Create ExecutorService
    private LinkedList<Future<Result>> results = new LinkedList<Future<Result>>(); // Collection of futures for results
    private long startTime;

    // initial region for which Mandelbrot is being computed
    private double x1 = -2.25;
    private double x2 =  3.0;
    private double y1 = -1.8;
    private double y2 =  3.3;

    private boolean done = false;   // computation finished?
    private int progress;           // number of scan lines
    private boolean drag = false;   // user dragging zoom box?

    // off-screen buffer and graphics
    private Image offscreen;
    private Graphics offg;

    public void init()
      { xsize = getSize().width;
        ysize = getSize().height;
        System.out.println("xsize = " + xsize + " ysize = " + ysize);
        taskSize = ysize / NUM_TASKS;

        // set up listeners
        this.addMouseListener(this);
        this.addMouseMotionListener(this);
      } // init

    public void start ()
      { // create offscreen buffer
        offscreen = createImage(xsize, ysize);
        if (offscreen == null)
          { System.err.println("ERROR: Cannot create offscreen image.");
            System.exit(1);
          }
        offg = offscreen.getGraphics();
        offg.setColor(Color.black);
        offg.fillRect(0, 0, xsize, ysize);

        // spawn thread to handle computations
        if (master == null)
          { master = new Thread(this);
            master.start();
          }
      } // start

    public void run()
      { while (true)
          { while (done)
              { try
                  { Thread.sleep(500);
                  }
                catch (InterruptedException e)
                  { // ignore
                  }
              }
            generateImage();
          }
      } // run

    /** New, improved generateImage method, which incorporates
      * the functionality of the old waitForResults method too.
      */
    private void generateImage()
      { startTime = System.currentTimeMillis();
        for (int i = 0; i < ysize; i += taskSize)
          { // Create tasks and hand over to executor
            Future<Result> res = exec.submit(new WorkerThread(i, i+taskSize));
            results.add(res);
          }
        // Now collect results
        while (results.size() > 0)
          { Future<Result> f = results.removeFirst();
            Result res = null;
            try
              { res = f.get();
              }
            catch (Exception exc)
              { System.err.println("ERROR: thread exception: " + exc);
                continue;
              }
            progress += taskSize;
            display(res.data, res.start);
          } // while

        done = true;
        long end = System.currentTimeMillis();
        System.out.println("Time taken: " + (end-startTime) + "ms.");
        repaint();
      } // generateImage

    public void mouseDragged (MouseEvent e)
      { int x = e.getX();
        int y = e.getY();
        if (done)
          { drag=true;
            Graphics g=this.getGraphics();
            g.drawImage(offscreen,0,0,this);
            g.setColor(Color.white);
            g.drawRect(x-xsize/4,y-ysize/4,xsize/2,ysize/2);
          }
      } // mouseDragged

    public void mouseReleased(MouseEvent e)
      { int x = e.getX();
        int y = e.getY();
        if (done)
          { x1 += ((float)x / (float)xsize) * x2;
            y1 += ((float)y / (float)ysize) * y2;
            x2 /= 2.0;
            y2 /= 2.0;
            x1 -= x2 / 2.0;
            y1 -= y2 / 2.0;
            done = false;
            drag = false;
            offg.setColor(Color.black);
            offg.fillRect(0, 0, xsize, ysize);
            progress = 0;
            repaint();
          }
      } // mouseReleased

    public void update (Graphics g)
      { if (!drag)
          { paint(g);
          }
      } // update

    public void paint (Graphics g)
      { if (!drag)
          { if (done)
              { g.drawImage(offscreen,0,0,this);
              }
            else
              { if (offscreen != null)
                  g.drawImage(offscreen,0,0,this);
                g.setColor(Color.white);
                g.drawRect(xsize/4, 10, xsize/2, 5);
                g.fillRect(xsize/4, 11, (progress*(xsize/2))/ysize, 4);
              }
          }
      } // paint

    private Color getPixelColour (int val)
      { Color colour;

        if (val == 100)
          colour = new Color(0,0,0); // Black for non-convergence
        else
          colour = Color.getHSBColor(val/100.0f, 1.0f, 1.0f); // Improved colour mapping
        /*
        else if (val > 90)  colour = new Color(val*2,0,(val-90)*25);
        else if (val > 80)  colour = new Color(val*2,0,0);
        else if (val > 60)  colour = new Color(val*3,0,val);
        else if (val > 20)  colour = new Color(val*4,0,val*2);
        else if (val > 10)  colour = new Color(val*5,0,val*10);
        else                colour = new Color(0,0,val*20);
        */
        
        return colour;
      } // getPixelColour

    private void display (byte[][] points, int start)
      { int j = 0;
        for(int l=start;j < taskSize; j++, l++)
          { for(int k = 0; k < xsize; k++)
              { int n = points[k][j];
                Color pixelColour = getPixelColour(points[k][j]);
                offg.setColor(pixelColour);
                offg.fillRect(k, l, 1, 1);
              }
          }
        repaint();
      } // display

    public void mousePressed(MouseEvent e) {}
    public void mouseClicked(MouseEvent e) {}
    public void mouseEntered(MouseEvent e) {}
    public void mouseExited(MouseEvent e) {}
    public void mouseMoved(MouseEvent e) {}

    public static void main (String args[])
      { Frame f = new Frame("Mandelbrot applet");
        f.addWindowListener(new WindowCloser());
        Applet a = new MandelbrotConc();
        a.setSize(800, 800);
        f.setSize(800, 800);
        f.add(a, BorderLayout.CENTER);
        a.init();
        f.setVisible(true);
        a.start();
      } // main

    // Inner classes

    private static class WindowCloser extends WindowAdapter
      {
        public void windowClosing (WindowEvent e)
          { System.exit(0);
          }
      } // inner class WindowCloser

    private class WorkerThread implements Callable<Result>
      { private int start;
        private int end;

        public WorkerThread (int start, int end)
          { this.start = start;
            this.end = end;
          } // constructor

        public Result call ()
          { double x, y, xx, a, b = y1, da = x2/xsize, db = y2/ysize;
            byte[][] results = new byte[xsize][end-start];

            for (int i = 0; i < start; i++)
              { b = b + db;
              }

            int k = 0;

            for (int i = start; i < end; i++, k++)
              { a = x1;
                for (int j = 0; j < xsize; j++)
                  { byte n = 0;
                    x = 0.0;
                    y = 0.0;
                    while ( (n < 100) && ( (x*x)+(y*y) < 4.0) )
                      { xx = x * x - y * y + a;
                        y = 2 * x * y + b;
                        x = xx;
                        n++;
                      }
                    results[j][k] = n;
                    a = a + da;
                  }
                b = b + db;
              }
            return new Result(results, start);
          } // call
      } // inner class WorkerThread
      
    /* Inner class to encapsulate results
     */
    private class Result
      { public byte[][] data;
        public int start;

        public Result (byte[][] data, int start)
          { this.data = data;
            this.start = start;
          } // constructor
      } // inner class Result

  } // class MandelbrotConc


George Wells, G.Wells@ru.ac.za