The aim of this exercise is:
The exercise contains three different parts: Synchronization, Buttons and Producers - Consumers. The first two parts, Synchronization and Buttons, are most important. The students who have not taken the Real-Time Programming course should also do Producers - Consumers.
In order to avoid class name conflicts we recommend that you create one subdirectory for each of the three main parts of this exercise: Synchronization, Buttons, and Producers - Consumers.
Synchronization and mutual exclusion are important when multiple threads access external units or shared variables. In Java mutual exclusion is primarily achieved by declaring methods as synchronized or by using synchronized blocks within methods.
Consider the following code example.
class Wrong extends Thread { private volatile int ti = 0; public void run() { int e; int u; int loops = 0; setPriority(4); try { e = 10; while (true) { if (ti != 0) { u = e / ti; } else { u = 0; } loops++; } } catch (Exception ex) { System.out.println("Terminated after " + loops + " iterations with " + ex); System.exit(1); } } public void setTi(int newTi) { ti = newTi; } public static void main(String arg[]) { Wrong w1 = new Wrong(); w1.start(); int i = 0; while (true) { w1.setTi(0); try { Thread.sleep(1); } catch (Exception e) {}; w1.setTi(2); try { Thread.sleep(1); } catch (Exception e) {}; i++; if (i > 100) { System.out.println("Main thread is alive"); i = 0; } } } }
Notice that the problems do not occur until after a while. This is due to the event-driven nature of concurrent real-time systems and one important reason why it is difficult to verify the correctness of real-time programs by testing alone.
The volatile keyword tells the compiler that the variable is manipulated by multiple threads and that once the variable has been written it is flushed out of the cache to main memory, so that it immediately becomes visible to other threads. It also prevents the compiler from allocating the variable in a register instead of in main memory. If the keyword is removed (try it) the above example will not work as intended.
As a general rule of thumb you should always use synchronization to protect accesses to shared variables, also in the case when you are quite sure that the access operations are atomic. The reason is that although the operations are atomic, the memory management model within the JVM in certain situations does not guarantee that the writing to the physical memory occurs in the same order as the order in which the changes take place. This may give confusing results.
The best approach is to introduce a new class, Screen, with a single synchronized method
public synchronized void writePeriod(int p);
that the periodic threads call instead of writing directly to the screen. Implement this class (in a separate file) and modify Periodic accordingly. Give the new version the name PeriodicWithScreen. It is important that you only have a single instance of the Screen class that all the periodic threads access in order to write. Create this single instance in the main method of PeriodicWithScreen and pass it in to the instances through the constructor.
Another approach is to use a static object within Periodic as a lock. Since the lock is static it will be shared between all instances of Periodic. This solution is used in PeriodicStaticLock. Make sure that you understand this code.
Another possibility is to instead synchronize directly on the class lock of Periodic. The class lock is the lock that is acquired when a synchronized static method executes. The class lock is a distinct lock that is different from all instance locks. It is possible to have a block of code synchronized on the class lock using the following notation
synchronized (this.getClass()) { ... }
A common way to achieve mutual exclusion is to use semaphores. Semaphores did not become part of Java until Java 1.5, and then only in the java.util.concurrent package. However, also before that the functionality could easily be implemented using synchronization. The class se.lth.control.realtime.Semaphore implements a standard counting semaphore. Study the Java documentation for the class.
A drawback with semaphores compared to synchronized method is that it is easy to forget to do a take() or give(). The compiler gives no debugging support regarding this. An advantage is that it is possible to provide mutual exclusion for scopes that are larger than the scope of a single method.
Note! It is highly recommended to read through the entire exercise before starting with any implementation.
As an exercise in real-time programming the following problem should be solved. The level in a tank should be controlled with a proportional controller. A suitable sampling interval for the controller is 0.1 seconds.
The reference signal is a square-wave with a period of 20 seconds. This should be generated internally in the program. With a set of increase and decrease buttons it should be possible to change the square-wave signal amplitude with a rate of 10 V per minute. The buttons should be polled periodically with a period of 10 ms.
It should be possible to start and stop the control with on-off buttons. Associated with the on-off buttons are status indication lamps.
It should be possible to type in new values of the controller gain K from the terminal (No graphical Swing-interface). The set-up is shown in the figure below.
The overall task is to write a Java program that solves the problem using threads and monitors. Use four public classes: Regul, OpCom, SquareWave, and Buttons, plus a Main class for initialization.
A first step in the design of the real-time problem is to decide which threads that are needed for the application. Some rules of thumb are:
The next step in the design is to identify the critical sections that need to be protected using monitors. Here we will use Java's synchronized methods to achieve the synchronization.
The OpCom thread should update the controller gain K when the user has entered a new value. The controller gain is also needed by the Regul thread. Therefore it is necessary to protect the controller gain using a separate monitor class, ParameterMonitor. This class should have two synchronized access methods, one set method, setK, and one get method, getK, i.e.
public synchronized void setK(double newK); public synchronized double getK();
The next item that needs protection is the reference value. It is accessed by both the Regul thread and by the SquareWave thread. Again we use a separate monitor class, ReferenceMonitor with two synchronized access methods:
public synchronized void setRef(double newRef); public synchronized double getRef();
We also need a boolean flag that decides whether the controller should be on or off. The flag is accessed by both the Regul thread and by the Buttons thread that polls the buttons. Let the monitor for this flag be named FlagMonitor, and have the following access methods:
public synchronized void setFlag(boolean value); public synchronized boolean isOn();
Finally, we need to represent the amplitude of the square-wave reference signal. This value needs to be accessed both by the SquareWave thread and by the Buttons thread. Again we introduce a monitor class, AmplitudeMonitor, with two synchronized access methods:
public synchronized void setAmp(double newAmp); public synchronized double getAmp();
Hence, we need four monitors, each represented as a separate object with synchronized access methods. Instead of using four separate monitors one could instead use a single monitor. However, this would create unnecessary blocking among the threads. For example, the OpCom thread calling setK would block a call from the SquareWave thread to setRef, which is not necessary from a synchronization point of view.
The next step in the design is to define the public interfaces between the different code modules. Here we will use the public classes Regul, OpCom, SquareWave, and Buttons as code modules. In order to minimize the number of top-level classes, and hence the number of files, we will implement the four monitor classes as internal classes within the top-level module classes.
Since it is quite natural to let the controller gain be the responsibility of the Regul class, we will implement ParameterMonitor as an internal class inside Regul. Quite arbitrarily, we also decide that the ReferenceMonitor and the FlagMonitor should be internal classes of Regul. The AmplitudeMonitor, finally, we implement as an internal class of SquareWave.
When encapsulating the monitors as internal classes we need extra methods at the module class level that simply redirect the call to the corresponding monitor access method. However, these redirection methods should not be synchronized. If they are, we can again create situations where the threads block each other in unnecessary ways.
Finally, we decide to implement all our thread classes by inheriting from the Thread class.
Analog and digital input and output are implemented in the classes AnalogIn, AnalogOut, DigitalIn, and DigitalOut in se.lth.control.realtime. The classes are used as in the code example below.
class IODemo { public static void main(String[] args) { AnalogIn yChan; AnalogOut uChan; DigitalIn digIn; DigitalOut digOut; try { uChan = new AnalogOut(1); yChan = new AnalogIn(1); digIn = new DigitalIn(1); digOut = new DigitalOut(1); } catch (Exception e) { System.out.println(e); } try { uChan.set(0.0); double y = yChan.get(); digOut.set(true); boolean b = digIn.get(); } catch (Exception e) { System.out.println(e); } } }
Each button is connected to one digital input. Each indicator lamp is connected to one digital output.
During the exercise you should, however, not use the physical IO. Instead you should run against a real-time simulation of a first-order system and you should use a graphical buttons box implemented as a Swing GUI.
The buttons box is provided by the class Box. (Download Box.java to your current directory. It is not necessary for you to open and study the code of this class.) The class provides six methods that each either returns a subclass to digital input representing a button or returns a subclass to digital output representing an indicator lamp. The implementation of Box is thread-safe and, therefore the methods can be called without any monitor protection. The methods are the following:
public DigitalButtonIn getOnButtonInput(); // On button input public DigitalButtonOut getOnButtonOutput(); // On lamp output public DigitalButtonIn getOffButtonInput(); // Off button input public DigitalButtonOut getOffButtonOutput(); // Off lamp output public DigitalButtonIn getIncButtonInput(); // Inc button input public DigitalButtonIn getDecButtonInput(); // Dec button input
DigitalButtonIn and DigitalButtonOut are subclasses to DigitalIn and DigitalOut respectively.
Let Regul manipulate the lamp outputs and Button manipulate the button inputs.
The simulated process is provided by the class FirstOrderProcess (Download FirstOrderProcess.java to your current directory. It is not necessary for you to open and study the code of this class.). This class provides the following methods:
// Argument 0: Returns the AnalogSource for the tank output. public AnalogSource getSource(int i); // Argument 0: Returns the AnalogSink for tank input. // Argument 1: Returns the AnalogSink for the reference signal. public AnalogSink getSink(int i);
AnalogSource and AnalogSink are subclasses to AnalogIn and AnalogOut respectively.
The simulated process is displayed by a window containing a plot of the control signal to the tank, the output (level) of the tank, and the reference signal of the controller.
The public interface to a code module can be defined in different, more or less formal ways, in Java.
In this example we will use the first, informal, approach. The interfaces of our four top level module classes, including their associated internal classes, should be as follows.
In addition to these classes the following Main class should be used to initialize the application.
import SimEnvironment.*; // packaged in VirtualSimulator.jar public class Main { public static void main(String argv[]) { final int regulPriority = 8; final int buttonsPriority = 7; final int squarePriority = 6; final int opcomPriority = 5; Box box = new Box(); FirstOrderProcess simulatedProcess = new FirstOrderProcess(); Regul regul = new Regul(regulPriority, box, simulatedProcess); OpCom opcom = new OpCom(regul, opcomPriority); SquareWave squarewave = new SquareWave(regul, squarePriority); Buttons buttons = new Buttons(regul, squarewave, buttonsPriority, box); regul.start(); opcom.start(); squarewave.start(); buttons.start(); } }
A nice overview is provided if the structure is illustrated graphically. Here we will use an approach where each module is represented by a bold rectangle. A monitor inside the module is represented by a rectangle inside the module. Public methods of the module are represented as rectangles that are aligned with the module border. Synchronized methods of the monitor are represented as rectangles that are aligned with the monitor border. Threads are represented as dashed ellipses. The graph for our application is shown below:
System.out.print("K = "); try { byte[] b = new byte[10]; System.in.read(b, 0, 10); regul.setK(Double.parseDouble(new String(b))); } catch (Exception e) { }
> javac -classpath .:virtualsimulator.jar *.java > java -classpath .:virtualsimulator.jar Main
Try with different values of the gain K. Does the result correspond to what you remember from the basic course in control (Reglerteknik AK)? What about the stationary error?
This exercise is primarily intended for those students who have not taken the Real-Time Programming course.
public class RingBuffer { public RingBuffer(int size); public synchronized Object get(); public synchronized void put(Object o); }
Implement the ring buffer as an array, a counter that contains the current number of elements in the buffer and two indexing variables that tell where data should be read and written respectively.
Represent the buffer as an array of Object references
private Object[] elements;
Use increment modulus the size of the buffer when updating an indexing variable, i.e.
inp = (inp + 1) % buffSize;