Round-Robin Scheduling

Our next examples show two ways of performing round-robin scheduling. These techniques are mostly useful for programs that will execute on a green-thread implementation of the Java virtual machine, since native-thread implementations all perform some sort of round-robin scheduling already. If you don’t know which implementation your program will eventually run on, you may want to use one of these techniques to ensure round-robin scheduling, even if the underlying implementation is performing it for you: these techniques are not in conflict with native-thread implementations, though they do not necessarily provide a benefit on those platforms.

Remember that at issue here is the behavior of a Java program that contains one or more CPU-intensive threads. A Java program could have hundreds of threads that may only periodically need access to the CPU and otherwise spend most of their life in the blocked state: in that case, there isn’t likely to be much competition for the CPU, and each thread gets an opportunity to run whenever it has work to do. We only face the problem of CPU starvation when there is at least one CPU-intensive thread that may potentially prevent all other threads from running.

If we have only a single CPU-intensive thread, there is no need for a complicated scheduling mechanism: all we need to do is lower the priority of the CPU-intensive thread below the priority of the other threads in our Java program. This allows the other threads to run whenever they have work to do, while the CPU-intensive thread continues to execute whenever the remaining threads are blocked. We’ll build on this principle in our scheduler class: our CPU-intensive threads will all have a lower priority than threads that are mostly blocked.

We’ll look at two schedulers in this section. The basic principle behind each scheduler is that each thread under its control is given a fixed amount of time during which it runs. When the specified time period elapses, another thread runs; this process proceeds indefinitely.

A Simple Round-Robin Scheduler

How do we go about creating a round-robin scheduler? Clearly, we need to begin with some sort of periodic timer; every time the timer goes off, we can make a different thread become the currently running thread. What do we need to do to make this happen?

The simplistic answer to this question is: nothing. That is, our simple scheduler is simply a high-priority timer that periodically wakes up only to go back to sleep immediately. This creates, in effect, a timer-based scheduling event: each time the timer thread wakes up, it becomes the currently running thread, which also adjusts the list of threads at the priority of the previously running thread:

public class SimpleScheduler extends Thread {
    int timeslice;

    public SimpleScheduler(int t) {
        timeslice = t;
        setPriority(Thread.MAX_PRIORITY);
        setDaemon(true);
    }

    public void run() {
        while (true)
            try {
                sleep(timeslice);
            } catch (Exception e) {}
    }
}

We’ll use this class in the example from the beginning of Chapter 6 so that we can illustrate its behavior:

class TestThread extends Thread {
    String id;

    public TestThread(String s) {
        id = s;
    }

    public void doCalc(int i) {
    }
    public void run() {
        int i;
        for (i = 0; i < 10; i++) {
            doCalc(i);
            System.out.println(id);
        }
    }
}

public class Test {
    public static void main(String args[]) {
        new SimpleScheduler(100).start();
        TestThread t1, t2, t3;
        t1 = new TestThread("Thread 1");
        t1.start();
        t2 = new TestThread("Thread 2");
        t2.start();
        t3 = new TestThread("Thread 3");
        t3.start();
    }
}

In this program there are three threads (t1, t2, and t3) at the Java default priority of NORM_PRIORITY, and the SimpleScheduler thread that runs at a priority of MAX_PRIORITY. The SimpleScheduler thread is normally blocked, so the list of threads starts out in this state:

               PRIORITY 5:  t2 -> t3 -> t1 -> NULL
  BLOCKED:  SimpleScheduler -> NULL

At this point, t1 is the currently running thread, and we’ll start to see output lines that say “Thread 1.” When SimpleScheduler wakes up, it moves to the runnable state and, because it is the highest priority thread in the Java virtual machine, it becomes the currently running thread:

               PRIORITY 5:  t2 -> t3 -> t1 -> NULL
PRIORITY 10:  SimpleScheduler -> NULL

SimpleScheduler immediately executes the sleep() method, moving it back to the blocked state; the Java virtual machine then selects the next thread in the list (t2) as the currently running thread and moves it to the end of the list:

               PRIORITY 5:  t3 -> t1 -> t2 -> NULL
  BLOCKED:  SimpleScheduler -> NULL

As this continues, each thread in the list of threads at priority 5 becomes the currently running thread in turn.

This scheduler requires that the virtual machine reorder the threads on a priority list whenever one of them is selected to run. As we mentioned in the last chapter, this is almost universally the case, but it is not a requirement of the Java specification, and we know of one real-time operating system on which this scheduling mechanism does not work.

Note that this mechanism still works for native-thread implementations. On a Windows implementation, the effect is that the currently running thread changes more often than specified by the sleep value within the SimpleScheduler, since the operating system will sometimes change the currently running thread while the scheduler is sleeping. On a Solaris implementation, the reordering of the threads will be dependent on the number of LWPs, but the interruption is sufficient to cause a single LWP to schedule another thread, which achieves the desired effect.

A More Complete Scheduler

Now we’ll look into building a more complete scheduler that will schedule our threads in a round-robin fashion. We can also use it to limit round-robin scheduling on native-thread platforms that timeslice as their default behavior; this limiting is achieved simply by using a very large value as the timeslice that the scheduler gives to a particular thread. However, since there are circumstances on native-thread platforms where the highest priority thread is not necessarily the currently running thread, we cannot completely prevent some sort of round-robin scheduling on those platforms: the best we can do is to use this scheduler to bias the operating system to favor one particular thread.

The example we outline in this section assumes that there is a single CPU. If you need to use this technique on a machine with multiple CPUs, you will need to adjust the scheduler so that it creates N currently running threads rather than one currently running thread (where N is the number of processors on the machine). As written, this technique will work on machines with multiple processors—that is, it will prevent any CPU starvation—but it will have less of an effect on the overall scheduling of the threads.

We’ll start building this scheduler by establishing threads at three priority levels:

Level 6

The scheduler itself is a separate thread running at level 6. This allows it to run in favor of the default threads created by the Java virtual machine and APIs and in favor of any threads the scheduler is controlling. This thread spends most of its time sleeping (i.e., blocked), so it doesn’t usually become the currently running thread.

Level 4

The scheduler selects one thread from all the threads it is controlling and assigns that thread a priority value of 4. Most of the time, this is the nonblocked thread with the highest priority in the Java virtual machine, so it is the thread favored to become the currently running thread.

Level 2

All remaining threads under control of our scheduler run at priority level 2. Since there is always a thread running at level 4, these threads usually do not run at this priority; they remain at this priority until they are selected by our scheduler to have a priority level of 4, at which time they become favored to be the currently running thread.

The idea behind the scheduler is that the programmer assigns certain threads to be under control of the scheduler. The scheduler selects one and only one of these threads and assigns it a priority of 4, while the rest of the threads have a priority of 2. The priority 4 thread is the currently running thread; from time to time, the scheduler itself wakes up and selects a different thread as the single priority 4 thread. On green-thread platforms, the priority 4 thread will always be selected as the currently running thread; on native-thread platforms, it will usually be selected as the currently running thread.

For all the threads in this scheduling system—the scheduler thread itself plus any threads the programmer designates to be controlled by our scheduler—it is clear that no CPU starvation will occur: the scheduler thread will always run when it needs to, and as long as that thread correctly adjusts the priorities of the remaining threads under its control, all other threads will get their opportunity to become the currently running thread.

In order to keep track of all the threads, we’ll use the CircularList we developed in Chapter 5. This class gives us the queueing behavior we need to keep track of the threads under the control of our scheduler: we can add threads to the list with its insert() method, remove them with its delete() method, and, more important, go through the list by repeatedly calling its getNext() method.

Here’s the first pass at our scheduler:

public class CPUScheduler extends Thread {
    private int timeslice;            // # of milliseconds thread should run
    private CircularList threads;     // All the threads we're scheduling

    public volatile boolean shouldRun = false; // Exit when this is set

    public CPUScheduler(int t) {
        threads = new CircularList();
        timeslice = t;
    }

    public void addThread(Thread t) {
        threads.insert(t);
        t.setPriority(2);
    }

    public void removeThread(Thread t) {
        t.setPriority(5);
        threads.delete(t);
    }

    public void run() {
        Thread current;
        setPriority(6);
        while (shouldRun) {
            current = (Thread) threads.getNext();
            if (current == null)
                return;
            current.setPriority(4);
            try {
                Thread.sleep(timeslice);
            } catch (InterruptedException ie) {};
            current.setPriority(2);
        }
    }
}

Although there are some necessary adjustments that we’ll add to this scheduler throughout the rest of this chapter, this code is the essence of the scheduler. The refinements that we’ll add are important in terms of making the class robust and thread safe, but they don’t add to the basic functionality: we want to understand the functionality before we look at some of the subtle issues involved in this class.

The programmer uses two methods to interface with the scheduler: addThread(), which adds a thread to the list of thread objects under control of the scheduler, and removeThread(), which removes a thread object from that list.[10]

Given this interface, we can use the CPUScheduler class in the ThreadTest class we introduced at the beginning of this section:

class TestThread extends Thread {
    String id;

    public TestThread(String s) {
        id = s;
    }

    public void doCalc(int i) {
    }

    public void run() {
        int i;
        for (i = 0; i < 10; i++) {
            doCalc(i);
            System.out.println(id);
        }
    }
}

public class Test {
    public static void main(String args[]) {
        CPUScheduler c = new CPUScheduler(100);
        TestThread t1, t2, t3;
        t1 = new TestThread("Thread 1");
        t2 = new TestThread("Thread 2");
        t3 = new TestThread("Thread 3");
        c.addThread(t1);
                       c.addThread(t2);
                       c.addThread(t3);
        t1.start();
        t2.start();
        t3.start();
        c.start();
    }
}

When our program calls c.start(), the CPUScheduler’s run() method gets called; it is this run() method that actually manipulates all the threads to create the timesliced, round-robin scheduling. At its base level, the logic for our scheduler is simple: it loops forever, going through all the threads in our circular list of threads and adjusting their priorities as it goes. In between, it sleeps for timeslice milliseconds. The current thread runs for that many milliseconds before the scheduler wakes up again and readjusts the thread’s priority. When there are no threads left to schedule—which would happen if the programmer had called removeThread() on all the threads previously added—the CPUScheduler exits by returning from the run() method.

Let’s examine how the four threads in our program—threads t1, t2, t3, and the CPUScheduler thread—will behave now. After we call the c.start() method, the threads in the program are in this state:

               PRIORITY 2:  t1 -> t2 -> t3 -> NULL
PRIORITY 6:  CPUScheduler -> NULL

As the highest priority thread in the program, the CPUScheduler thread is the currently running thread. It starts executing the run() method, where the first thing it does is change the priority of thread t1 to 4:

               PRIORITY 2:  t2 -> t3 -> NULL
PRIORITY 4:  t1 -> NULL
PRIORITY 6:  CPUScheduler -> NULL

The CPUScheduler, still the currently running thread, now sleeps, placing it into the blocked state. This causes t1 to become the currently running thread:

               PRIORITY 2:  t2 -> t3 -> NULL
PRIORITY 4:  t1 -> NULL
   BLOCKED:  CPUScheduler -> NULL

When the CPUScheduler thread wakes up, it changes the priority of t1 back to 2 and the priority of t2 to 4:

               PRIORITY 2:  t3 -> t1 -> NULL
PRIORITY 4:  t2 -> NULL
PRIORITY 6:  CPUScheduler -> NULL

And so the cycle continues.

Adjustment 1: Synchronizing data within the CPUScheduler

Now that we have the base logic of the CPUScheduler written correctly, we need to make sure the CPUScheduler class is itself thread safe and that we haven’t introduced any race conditions into the scheduler by having incorrectly synchronized data. We’ll go through this process in a series of stages because the example illustrates the necessary steps that you must take in designing any class to work with multiple threads.

At first glance, there don’t appear to be any variables that need synchronization: the only instance variable that needs to be protected is the variable threads, and all changes to the threads variable occur via methods of the CircularList class that are already synchronized. But what would happen if you called the remove-Thread() method and removed the thread that the CPUScheduler has marked as the current thread? It would be an error for the CPUScheduler to change the priority of this thread once it has been removed from the threads list, so the removeThread() method must somehow inform the CPUScheduler that the current thread has been removed.

This means that the variable current must become an instance variable so that both the run() and removeThread() methods can access it. We can then synchronize access to that variable. Here’s the new CPUScheduler class:

public class CPUScheduler extends Thread {
    ...
    private Thread current;
    public void removeThread(t) {
        t.setPriority(5);
        threads.delete(t);
        synchronized(this) {
                              if (current == t)
                                  current = null;
                          }
    }
    ...
    public void run() {
        ...
        try {
            Thread.sleep(timeslice);
        } catch (InterruptedException ie) {};
        synchronized(this) {
                              if (current != null)
                                  current.setPriority(2);
                          }
    }
}

Alternatively, we could make the run() and removeThread() methods themselves synchronized:

public synchronized void run() {
        ...
    }

    public synchronized void removeThread(Thread t) {
        ...
    }

As we’ve seen, making the run() method synchronized is typically a bad idea, so we’ll reject this idea for now, but we’ll be revisiting this decision soon.

Adjustment 2: Making CPUScheduler thread safe

We’ve synchronized all the variables of our CPUScheduler, but we’re still not protected from threads that exit while they are under our control.

In particular, the run() method changes the priority of a thread, which is a valid operation only if a thread is in the runnable state. What happens if the thread that we’ve assigned to level 4 exits its run() method while our CPUScheduler is sleeping? When the CPUScheduler wakes up, it tries to set the priority of that thread, which is now in the exiting state, to 2—an operation that generates an exception. Similarly, if the thread that is running decides to call the stop() method of one of the priority 2 threads in the CPUScheduler’s list, then the next time the CPUScheduler selects that thread and sets its priority, we’ll get an exception.

So we need to place all the calls to the setPriority() method inside a try/catch clause in order to be alerted to these types of situations. This means we must modify our code everywhere we call the setPriority() method:

public void removeThread(Thread t) {
        try {
                              t.setPriority(5);
                          } catch(Exception e) {}
        threads.delete(t);
        synchronized(this) {
            if (current == t)
                current = null;
        }
    }

    public void run() {
        while (shouldRun) {
            ...
            try {
                                  current.setPriority(4);
                              } catch (Exception e) {
                                  removeThread(current);
                              }
            ...
            synchronized(this) {
                if (current != null)
                    try {
                                          current.setPriority(2);
                                      } catch (Exception e) {
                                          removeThread(current);
                                      }
            }
            ...
        }
    }

Note that in in the run() method, when the exception is thrown we need to remove the thread from the list of threads we’re interested in, which means that we must also use the catch clause in the removeThread() method.

Adjustment 3: More thread-safe modifications

We’ve made the methods of the CPUScheduler thread-safe, but what about the class itself? What if two threads try to create a CPUScheduler? This would be very confusing: we’d end up with two scheduling threads that would compete with each other to schedule other threads. So we need to allow only one instance of the class to be instantiated. We’ll do this by creating a static variable in the class and testing it to make sure that an instance of the CPUScheduler class doesn’t already exist. Because we can’t make the constructor itself synchronized, we’ll also need to introduce a synchronized method to access this static variable. Thus the constructor and related code for the class now look like this:

public class CPUScheduler extends Thread {
    private static boolean initialized = false;
                      private synchronized static boolean isInitialized() {
                          if (initialized)
                              return true;
                          initialized = true;
                          return false;
                      }

    public CPUScheduler(int t) {
        if (isInitialized())
                              throw new SecurityException("Already initialized");
        threads = new CircularList();
        timeslice = t;
    }
}

Adjustment 4: Devising an exit mechanism

If all the threads under its control exit, the CPUScheduler itself exits. In a program where the tasks are well defined at the beginning of execution—like the TestThread class we’ve looked at so far—that might be fine. But what if we wanted to add the CPUScheduler to our TCPServer? As presently written, the CPUScheduler wouldn’t work for that case: as soon as no clients were connected to the TCPServer, the CPUScheduler would exit, and any further clients that connected to the server would not be timesliced.

Instead, we need to make the CPUScheduler a daemon thread and adjust the logic of its run() method. This should make sense: the CPUScheduler is only useful when there are other threads in the program that it can schedule. In the TCP-Server case, there will always be at least one other thread in the program: the listener thread of the TCPServer. That listener thread creates other threads for the CPUScheduler to manipulate as clients connect to the server. The implementation of our timesliced TCPServer to perform calculations looks like this:

import java.net.*;
import java.io.*;

public class CalcServer {
    public static void main(String args[]) {
        CalcRequest r = new CalcRequest();
        try {
            r.startServer(3535);
        } catch (Exception e) {
            System.out.println("Unable to start server");
        }
    }
}

class CalcRequest extends TCPServer {
    CPUScheduler scheduler;
    CalcRequest() {
        scheduler = new CPUScheduler(100);
        scheduler.start();
    }

    void doCalc(Socket s) {
    }

    public void run(Socket s) {
        scheduler.addThread(Thread.currentThread());
        doCalc(s);
    }
}

Every time the run() method of the CalcRequest class is called, it is called in a new thread, so we need to add that thread to the CPUScheduler that was created in the constructor of the class. As long as the CPUScheduler doesn’t exit when there are no threads to schedule (which now means simply that no client is currently connected), we’ll have a timesliced calculation server. During an active session of our CalcServer, we’ll have these threads:

One listener thread

The thread that waits for connections and creates the client threads.

Zero or more client threads

These threads execute the calculation on behalf of a connected client.

CPUScheduler thread

The daemon thread performing the scheduling.

We can gracefully shut down the CalcServer by setting the shouldRun flag of the server to false; eventually the client threads complete their calculation and exit. When all the client threads have exited, only the daemon CPUScheduler thread remains in the program, and the program terminates.

We need to change the CPUScheduler so that instead of returning when there are no threads to be scheduled, it simply waits for more threads. Here’s the entire code for the modified CPUScheduler class (we’ll show the entire class here, since at this point, we have a complete implementation):

public class CPUScheduler extends Thread {
    private CircularList threads;
    private Thread current;
    private int timeslice;
    private static boolean initialized = false;
    private boolean needThreads;

    private static synchronized boolean isInitialized() {
        if (initialized)
            return true;
        initialized = true;
        return false;
    }

    public CPUScheduler(int t) {
        if (isInitialized())
            throw new SecurityException("Already initialized");
        threads = new CircularList();
        timeslice = t;
        setDaemon(true);
    }

    public synchronized void addThread(Thread t) {
        t.setPriority(2);
        threads.insert(t);
        if (needThreads) {
                              needThreads = false;
                              notify();
                          }
    }

    public void removeThread(Thread t) {
        threads.delete(t);
        synchronized(this) {
            if (t == current)
                current = null;
        }
    }

    public synchronized void run() {
        setPriority(6);
        while (true) {
            current = (Thread) threads.getNext();
            while (current == null) {
                                  needThreads = true;
                                  try {
                                      wait();
                                  } catch (Exception e) {}
                                  current = (Thread) threads.getNext();
                              }
            try {
                current.setPriority(4);
            } catch (Exception e) {
                removeThread(current);
                continue;
            }
            try {
                wait(timeslice);
            } catch (InterruptedException ie) {};
            if (current != null) {
                try {
                    current.setPriority(2);
                } catch (Exception e) {
                    removeThread(current);
                }
            }
        }
    }
}

In the constructor, we’ve set the thread to be a daemon thread—the point of this adjustment. Note that we also changed the run() method so that when we try to retrieve a thread from the list, we loop until one is available. If no thread is in the list, we wait until one is available, which requires that we add a flag to the addThread() method to signify whether it should notify the CPUScheduler thread that a thread has been added.

In addition, note that we’ve changed the run() method itself to a synchronized method and replaced the call to the sleep() method with a call to the wait() method. This is one example of the exception to the general rule that the run() method should not be synchronized: since we actually spend more time waiting in this method than executing code, its quite okay to synchronize the run() method, since it will release the lock whenever it waits for something to happen.

Adjustment 5: Non-CPU-intensive threads

What happens in our scheduler if the currently running thread blocks? Let’s see what would happen to our TestThread program if the currently running thread suddenly entered the blocked state. We’d start out with the threads in a state like this:

                  PRIORITY 2:  t3 -> t1 -> NULL
PRIORITY 4:  t2 -> NULL
   BLOCKED:  CPUScheduler -> NULL

Thread t2 is the currently running thread, executing its calculations while the CPUScheduler is sleeping. If t2 now enters the blocked state for some reason, we end up with threads in this state:

                  PRIORITY 2:  t3 -> t1 -> NULL
PRIORITY 4:  NULL
   BLOCKED:  t2 -> CPUScheduler -> NULL

This means that t3 becomes the currently running thread, even though it’s at priority 2. When the CPUScheduler wakes up, it resets the priority of t2 to 2, sets the priority of t3 to 4, and goes back to sleep, leaving our threads in this state:

                  PRIORITY 2:  t1 -> NULL
PRIORITY 4:  t3 -> NULL
   BLOCKED:  t2 -> CPUScheduler -> NULL

Everything is okay again, but at some point it will be t2’s turn to be priority 4. Since the CPUScheduler has no way of determining that t2 is blocked, it sets the priority of t2 to 4. The Java scheduler again selects one of the threads at priority 2 to be the currently running thread.

Our code was correct: the threads involved all got some timeslice in which to run. But there was a short period of time during which the CPUScheduler slept, the priority 4 thread blocked, and a priority 2 thread became the currently running thread. In effect, this priority 2 thread stole some CPU time; it could do this because there was a time gap between when the priority 4 thread blocked and the priority 6 thread woke up.

It’s probably not a crisis that this happened, since once the CPUScheduler woke up, we got back to the thread state we wanted. We could have prevented this CPU stealing from happening if somehow we knew when the priority 4 thread had blocked. However, on a native-thread platform, we cannot prevent a lower-priority thread from running at some point anyway, which is really just a variation of the behavior that we’re discussing here. So solving this problem is not something that we’ll be able to do in an absolute sense.

It is conceivable that on a green-thread platform, we could create a new thread within the CPUScheduler class at priority 3. When the priority 4 thread blocks, this priority 3 thread would become the currently running thread; this priority 3 thread could inform the priority 6 thread that it should wake up and perform some scheduling. Note that on a native-thread platform this does not work: the priority 3 thread might still run even if the priority 4 thread has not blocked, and on a Windows platform, priority 3 and 4 share the same underlying operating system priority. Altering the priority levels of the threads to avoid this overlap—by, for example, running the scheduler at priority 8 and the target thread at priority 6—is a possibility, but we’ve seen that putting a CPU-intensive thread above the default priority level (especially the level at which the system GUI thread runs) is not always a good idea. And this does not prevent the priority 3 thread from running when the target thread is not blocked.

Even on a green-thread platform, this problem is impossible to solve in the general case. If all the threads to be scheduled were to block, then the priority 3 thread would continually run, consuming a lot of CPU resources but performing no real work. In the first edition of this book, we showed how to overcome that problem by suspending the priority 3 thread, but now the suspend() method has been deprecated, so that solution is no longer an option. And since the benefit provided by such a solution would be very marginal, we’re not too worried that such a solution does not exist.

The moral of the story is what we’ve said all along: Java’s scheduling mechanisms give you some control over how threads are scheduled, but that control is never absolute.



[10] There’s a subtle error here, in that when the thread is removed from the scheduler, we assign it the default thread priority rather than the priority it had when it was added to the scheduler. The correct practice would be to save the thread’s priority in the call to the addThread() method and then restore that priority in the removeThread() method; we’ll leave that implementation to the reader.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset