DSF Common Patterns

Summary

Examples

Running example code is and performing included excercises is very helpful in following this tutorial.  In order to run the examples in this tutorial the following is needed:
  1. Download and install Eclipse development environment, either the Eclipse Classic 3.4 or Eclipse IDE for C/C++ Developers
  2. Install the DSF SDK feature to build against, by performing either:
    1. Using update manager, install the Debugger Services Framework end-user and extender SDK, found in the Ganymede Discovery Site under Remote Access and Device Development.
    2. Check out org.eclipse.dd.dsf and org.eclipse.dd.dsf.ui plugins, found in the /cvsroot/dsdp repository under org.eclipse.dd.dsf/plugins directory.
  3. Check out the org.eclipse.dd.examples.dsf plugin, found /cvsroot/dsdp under org.eclipse.dd.dsf/plugins directory.
  4. Build the examples plugin:
    1. Execute the build the first time to build and run the excercises preprocessor.
    2. Refresh the resources in the plugin (right-click on project in Navigator and select Refresh), in order to recognize the sources generated by the preprocessor.
    3. Build the plugin again to compile the generated sources.
  5. Launch the examples
    1. Examples in data org.eclipse.dd.examples.dsf.requestmonitor and org.eclipse.dd.examples.dsf.dataviewer packages each contain a public main() function.  They can be launched using the Java Application launch type.
    2. TODO: Launching timers example

Asynchronous Methods

One of the central features of DSF is that it relies very heavily on the use of asynchronous methods.  Asynchronous methods here mean simply methods that use a callback object to indicate their completion. The use of asynchronous methods can be very contageous in a system, where if a lower level API is composed of asynchronous methods, a higher level system which uses those methods also has to have asynchronous methods in its interface (or risk blocking its calling thread).

TODO? : diagram of a layered system with asynchronous APIs

Request Monitor

There is a standard callback object used in DSF, the request monitor.  A request monitor has the following features:
Following is the snippet from a the "hello world" example of using a request monitor:
org.eclipse.dd.examples.dsf.requestmonitor.AsyncHelloWorld

 26: public class AsyncHelloWorld {

28: public static void main(String[] args) {
29: Executor executor = ImmediateExecutor.getInstance();
30: RequestMonitor rm = new RequestMonitor(executor, null);
31: asyncHelloWorld(rm);
32: }

34: static void asyncHelloWorld(RequestMonitor rm) {
35: System.out.println("Hello world");
36: rm.done();
37: }

Excercise 1: A common problem in DSF is implementing nested asynchronous methods, this excercise adds a second-level asynchronous method to AsyncHelloWorld. 

Look for comments preceeded with "// TODO Excercise 1" in the org.eclipse.dd.examples.dsf.requestmonitor.AsyncHelloWorld module.


Data Request Monitor

The base request monitor is useful for returning status of the asynchronous method, but they do not have an option of returning a value to the caller.  DataRequestMonitor can be used for that purpose. A simple example of using the data request monitor:

org.eclipse.dd.examples.dsf.requestmonitor.Async2Plus2

 22: public class Async2Plus2 {
23:
24: public static void main(String[] args) {
25: Executor executor = ImmediateExecutor.getInstance();
26: DataRequestMonitor<Integer> rm =
27: new DataRequestMonitor<Integer>(executor, null) {
28: @Override
29: protected void handleCompleted() {
30: System.out.println("2 + 2 = " + getData());
31: }
32: };
33: asyncAdd(2, 2, rm);
34: }

36: static void asyncAdd(int value1, int value2, DataRequestMonitor<Integer> rm) {
37: rm.setData(value1 + value2);
38: rm.done();
39: }
40: }

Multi-Request Monitor

A common problem when using asynchronous is that several asynchronous methods need to be called in parallel, so the calling method needs to somehow manage the completion of several request monitors.  CountingRequestMonitor can be used for this purpose.  It is configured such that it's done() method needs to be called a count number of times before the callback method is invoked. 
The following snipped from the AsyncQuicksort example shows a simple example of using the CountingRequestMonitor:

org.eclipse.dd.examples.dsf.requestmonitor.AsyncQuicksort.asyncQuickSort()

 42:     static void asyncQuicksort(final int[] array, final int left, 
43: final int right, final RequestMonitor rm)
44: {
45: if (right > left) {
46: int pivot = left;
48: int newPivot = partition(array, left, right, pivot);
49: printArray(array, left, right, newPivot);

51: CountingRequestMonitor countingRm = new CountingRequestMonitor(fgExecutor, rm);
52: asyncQuicksort(array, left, newPivot - 1, countingRm);
53: asyncQuicksort(array, newPivot + 1, right, countingRm);
54: countingRm.setDoneCount(2);
55: } else {
56: rm.done();
57: }
58: }
Excercise 2: Converting a synchronous method into an asynchronous one, is another common task in DSF.  This excercise converts the AsyncQuicksort.partition() method into asynchronous AsyncQuicksort.asyncPartition(). 

Look for comments preceeded with "// TODO Excercise 2" in the org.eclipse.dd.examples.dsf.requestmonitor.AsyncQuicksort module.

Concurrency

The simple examples in previous section used asynchronous method signatures, however no real asynchronous work was performed since all execution was performed in the main thread.  This section examines a more typical example of a problem that DSF is intended to solve: a viewer and an asynchronous data generator.

The IDataGenerator interface contains the following two asynchronous data access methods:

org.eclipse.dd.examples.dsf.dataviewer.IDataGenerator

 49:  void getCount(DataRequestMonitor<Integer> rm);
50: void getValue(int index, DataRequestMonitor<String> rm);

The example is intended to simulate a realistic problem therefore, it can be assumed that these methods do not complete the request monitor immediately, but rather that the requests are completed on a separate thread and with some delay.  There are two implementations of this service provided:

  1. DataGeneratorWithThread - Uses a java thread directly and various synchronization mechanisms for data integrity.
  2. DataGeneratorWithExecutor - Uses a DSF executor for both asynchronous execution and synchronization.
There are also two viewers provided which display data from the data generator:
  1. SyncDataViewer - Table-based viewer which implements a synchronous IStructuredContentProvider interface.
  2. AsyncDataViewer - Table-based viewer which implements an asynchronous ILazyContentProvider interface.

Query

DSF is designed to facilitate use of asynchronous APIs.  However, sometimes there are situations where a synchronous method has to be implemented to call an asynchronous method.  One utility used to accomplish this is a DSF Query object.  The Query object is meant to be extended by clients in order to override the asynchronous execute() method. The client code using a query can use the execute() implementation in order to call other asynchronous methods.  The following snippet from SyncDataViewer.getElements()  shows the use of Query:

org.eclipse.dd.examples.dsf.dataviewer.SyncDataViewer.getElements()

 59:         // Create the query object for reading data count. 
60: Query<Integer> countQuery = new Query<Integer>() {
61: @Override
62: protected void execute(DataRequestMonitor<Integer> rm) {
63: fDataGenerator.getCount(rm);
64: }
65: };
66:
67: // Submit the query to be executed. A query implements a runnable
68: // interface and it has to be executed in order to do its work.
69: ImmediateExecutor.getInstance().execute(countQuery);
70: int count = 0;
71:
72: // Block until the query completes, which will happen when the request
73: // monitor of the execute() method is marked done.
74: try {
75: count = countQuery.get();
76: } catch (Exception e) {
77: // InterruptedException and ExecutionException can be thrown here.
78: // ExecutionException containing a CoreException will be thrown
79: // if an error status is set to the Query's request monitor.
80: return new Object[0];
81: }

Image 1: Sequence diagram of Query use in getElements().



Note: Using the query object requires a great deal of care because calling a blocking method can create performance problems and raises possibility of deadlock. One common deadlock scenario occurs when the get() method is being called by a thread which is itself required for completion of the asynchronous methods called by execute().

TODO ?: add a sequence diagram of the deadlock scenario

Synchronization

Managing race conditions and deadlocks is one of the most challanging problems of large multi-threaded systems.  DSF uses a single-threaded executor as the primary mechanism for safe-guarding access to data.  Methods, which need to access data protected by the DSF executor, have to access this data inside a runnable submitted to the executor thread.  The following is an example of this from the DataGeneratorWithExecutor:

org.eclipse.dd.examples.dsf.dataviewer.DataGeneratorWithExecutor.addListener()

174:     public void addListener(final Listener listener) {
175: try {
176: fExecutor.execute( new DsfRunnable() {
177: public void run() {
178: fListeners.add(listener);
179: }
180: });
181: } catch (RejectedExecutionException e) {}
182: }
Note: It is immediately apparent that this synchronization mechanism adds a lot of overhead and for such a simple example, it is much less efficient than using a synchronized section or an atomic variable.  It is less obvious how this mechanism adds value, however this document is just a tutorial so the discussion of the merits of the design will be left out.

Image 1: Synchronization using multiple locks on data.
Image 2: Synchronization using a single DSF executor thread.
Comparing other parts of the two data generator implementation shows that using the synchronization mechanism above is the principal difference between the two implementations.  One notable exception is the principal processing loop in each data generator.  In the thread-based implementation this loop is implemented in the run method of the generator's thread:

org.eclipse.dd.examples.dsf.dataviewer.DataGeneratorWithThread.run()

139:     public void run() {
140: try {
141: while(true) {
142: // Get the next request from the queue. The time-out
143: // ensures that that the random changes get processed.
144: final Request request = fQueue.poll(100, TimeUnit.MILLISECONDS);
145:
146: // If a request was dequeued, process it.
147: if (request != null) {
148: // Simulate a processing delay.
149: Thread.sleep(PROCESSING_DELAY);
150:
151: if (request instanceof CountRequest) {
152: processCountRequest((CountRequest)request);
153: } else if (request instanceof ItemRequest) {
154: processItemRequest((ItemRequest)request);
155: } else if (request instanceof ShutdownRequest) {
156: // If shutting down, just break out of the while(true)
157: // loop and thread will exit.
158: request.fRequestMonitor.done();
159: break;
160: }
161: }
162:
163: // Simulate data changes.
164: randomChanges();
165: }
166: }
167: catch (InterruptedException x) {}
168: }

In contrast the executor-based generator uses a dedicated method for servicing the queue, which is called by every method that adds a new request to the queue:

org.eclipse.dd.examples.dsf.dataviewer.DataGeneratorWithExecutor.serviceQueue()

197:     private void serviceQueue() {
...
201: // If a queue servicing is already scheduled, do nothing.
202: if (fServiceQueueInProgress) {
203: return;
204: }
205:
206: if (fQueue.size() != 0) {
207: // If there are requests to service, remove one from the queue and
208: // schedule a runnable to process the request after a processing
209: // delay.
210: fServiceQueueInProgress = true;
211: final Request request = fQueue.remove(0);
212: fExecutor.schedule(
213: new DsfRunnable() {
214: public void run() {
215: if (request instanceof CountRequest) {
216: processCountRequest((CountRequest)request);
217: } else if (request instanceof ItemRequest) {
218: processItemRequest((ItemRequest)request);
219: }
220:
221: // Reset the processing flag and process next
222: // request.
223: fServiceQueueInProgress = false;
224: serviceQueue();
225: }
226: },
227: PROCESSING_DELAY, TimeUnit.MILLISECONDS);
228: }
229: }
Note: When using a single-threaded executor as the synchronization method very few other synchronization mechanisms need to be used.  For example the DataGeneratorWithExecutor.fQueue member is just a plain un-synchronized list.  This is true even when using background threads to perform long-running tasks, as long as these background threads can call a request monitor when finished.

Excercise 3: One benefit of the single-threaded executor concurrency model is that as long as a method is guaranteed to run in the executor thread, this method may access and modify any of the variables protected by this executor.  This excercise demonstrates performing a somewhat more complicated operation on protected state data.

Look for comments preceeded with "// TODO Excercise 3" in the org.eclipse.dd.examples.dsf.dataviewer.DataGeneratorWithExcecutor module.

Annotations

In any multi-threaded system it can become very difficult to determine what are the rules governing access to the various data objects.  In a DSF system, it is even more important to identify which data objects can only be accessed using a designated DSF executor.  Since there is no Java language mechanisms for this purpose, DSF defines a number annotations that can be used for this purpose.  The annotations are hierarchical, so that if a class has a given annotation in its declaration, its members and fields are assumed to have the same access restriction unless otherwise specified.

DSF synchronization annotations defined in org.eclipse.dd.dsf.concurrent


Note: The DSF synchronization annotations are no more than a comment intended to help make the code more understandable and maintainable.  Unfortunately, since there is no compiler enforcment of their presence, it is easy to forget to add them.


Excercise 4: This excercise adds the appropriate synchronization annotations to the methods and fields of DataProviderWithExecutor.

Look for comments preceeded with "// TODO Excercise 4" in the org.eclipse.dd.examples.dsf.dataviewer.DataGeneratorWithExcecutor module.



Excercise 5: It is all too easy to get into a deadlock situation.  This excercise purposefully puts the data viewer system into a deadlock.  The deadlock first renders the data viewer unusable, but the main thread also gets deadlocked when attempting to exit the program.

Look for comments preceeded with "// TODO Excercise 5" in the org.eclipse.dd.examples.dsf.dataviewer.SyncDataViewer module.

Services

OSGi

Session

Tracker

Data Model

View Model

Adapter, Provider, Node

Timers


org.eclipse.dd.examples.dsf.requestmonitor.Async2Plus2






Excercise abc: xyz