![]() |
Tutorial08 | Threads |
From a user's perspective, modern operating systems are able to simultaneously run many programs at "the same time." The truth is that each CPU registered with the operating system can only handle one thread of execution at a time. The illusion of simultaneity is achieved because the operating system nimbly juggles the many active threads. The analysis and design of the scheduling algorithms that set the order in which threads are executed is a fertile area of computer science research.
Individual programs may themselves consist of multiple threads of execution. For instance, a web browser might have one thread that handles the networking and another that handles user input. Keeping these parts of the program in separate threads allows the program to process user input (setting the preferences, for instance) while the other thread is waiting for a request to complete (retrieving the contents of a web page).
When using mxj, input from or output to the Max environment can take place in one of two threads. The main thread handles the least time-critical jobs, such as rendering to the screen or processing keyboard and mouse events. The scheduler thread handles time-sensitive data: midi data and the output of clocked objects (metro, delay, etc).
The code below is from the WhichThread example provided in the classes directory.
import com.cycling74.max.*;
public class WhichThread extends MaxObject implements Runnable
{
private static final String SCHEDULER = "SCHEDULER";
private static final String MAIN = "MAIN";
private static final String UNKNOWN = "????";
public void bang() {
if(MaxSystem.inTimerThread())
outlet(0,SCHEDULER);
else if(MaxSystem.inMainThread())
outlet(0,MAIN);
else
outlet(0,UNKNOWN);
}
public void run() {
this.bang();
}
public void spawnThread() {
Thread t = new Thread(this);
t.start();
}
}
The bang method illustrates how to use the inTimerThread and inMainThread methods from MaxSystem to determine the thread of execution. Each possibility for the thread of execution produces a different output - interacting with the WhichThread.help patch can be instructive.
If you click on either bang in the help patch, the bang method will be called in the main thread and "MAIN" will be output. If overdrive is enabled, starting the metro in the help patch will call the bang method from the scheduler thread, and "SCHEDULER" will be output. Notice that if you turn overdrive off "MAIN" is output.
WhichThread also illustrates one way that a class can create its own thread. Note that the class implements Runnable - very much like the Executable interface that we discussed in previous tutorials, Runnable is used to provide a common calling framework for thread-based classes. To implement Runnable a class must contain a public run method which will be executed when a Thread is started. In the case above, when spawnThread is called a new instance of the Thread class is created, with the WhichThread object itself passed to the constructor method via the "this" keyword. The next line calls this new Thread's start method - this is how you tell a Thread to start executing. The start method calls the run method of the Runnable that is was passed when it was constructed; since we passed in the object itself, WhichThread's run method is called, which then calls the bang method. So in this case bang has been called from neither the main nor the scheduler thread, and so "????" is output.
If you click on the "spawnThread" message in the help patch you'll notice that "????" does indeed get sent out the object's outlet. However, you'll also notice that the construction on the right, which takes the output of the first WhichThread object, triggers a bang and feeds that into another WhichThread. After clicking on the "spawnThread" message one might expect the output of this second WhichThread to also be "????", since the thread that initiates the action is neither the main nor the scheduler thread. Instead "MAIN" is output, which indicates that this second bang is received by the second WhichThread object in the main thread. This is so because MaxObject's outlet method will only output messages into the Max world in the main or scheduler threads. Any outlet call not made from within either of those two threads is deferred to the main thread.
It is important to understand that it is possible for a thread to be interrupted at any time by another thread. For example, if the operating system decides that it's time to pay attention to the scheduler thread instead of the main thread, execution of the main thread will be suspended and the scheduler thread will take over. When the main thread resumes it will pick up exactly where it left off. This can lead to some unstable situations. Consider the following class:
public class threadProblem extends MaxObject {
private int[] data = new int[10];
public void bang() {
int size = data.length;
for (int i=0;i size;i++) {
data[i] = i;
}
}
public void inlet(int i) {
data = new int[i];
}
}
An int sent in the object's inlet will create a new array of the given length, and a bang will populate the array for numbers. However, consider what would happen if a bang in the main thread (caused by user input, say) was interrupted sometime while executing the bang method and in the scheduler thread a integer was input? If the input is smaller than the previous size of the array, an exception will be thrown shortly after the flow of execution has passed back to the main thread when the for loop exceeds the end of the new array length. If the input is larger than the previous length no exception will be thrown, but the values of the array beyond the old length or that have already been set with the for loop will be uninitialized. Either way the results are unwelcome, so it is important to design your classes so that an unlucky flow of execution does not leave the data in an indeterminant state.