Keywords:
active objects, concurrency, program modeling, program analysis,
random walking, deadlocks, CORBA
- Active objects
- The application in local mode
- Debugging the application (error + solution)
- The application in distributed mode (error + solution)
- CORBAlization of the application
Definition of the IDL interface
Code segments extracted from the file produced by the IDL compiler- Graphical architecture editor
- Model checking
- Conclusion
This exercise shows how to analyze a distributed application based on active objects with the aim of detecting potential deadlocks.
The application we consider is distributed. It allows a group of users to see, on their own display, a shared drawing that they all edit simultaneously. In order to assure that all commands are processed in the same order, the system has a central monitor that collects the commands produced by the users and broadcasts them back to. Even the commands that come from the local user pass through the monitor before being applied locally.
In this exercise, we will only analyze the structure of the application without any internal details because the deadlock only depends on the ordering of the actions, and not on the details of the variables.
We will present successively five steps: the first one has a deadlock in the local mode already, the second one fixes the problem, the third one simulates a remote method call to reach the monitor, which again creates the conditions of a potential deadlock, the fourth one solves this problem and finally the last one is implemented on top of our own CORBA library.
This briefe description of active objects should be sufficient to understand the following sections. An active object has a special method, the body, defined much like a constructor/destructor, that is running in parallel with the bodies of the other objects. A body can call the methods of the other objects, exactly like passive objects. However, an active object has the capability to postpone the execution of the calls of its methods: A call cannot be executed as long as the body of the called object does not accept the call. In order to avoid blocking (see below), a select statement allows the parallel waiting of calls to other objects and acceptations of calls from the other bodies. Active objects can be compared to processes, but with method calls as inter-process communication means.
The first version of the application depicted on the figure below simulates an application running locally. Its communications are made by method calls. All objects are active. Three users get drawing elements from objects Graph, which get themselves their information from mouse clicks. The drawing commands are transmitted to the monitor which broadcasts it to all users, including the initiator of the request. Each user accepts the calls from the monitor in parallel with the waiting of elements from the graphical module, in order to be ready to serve a broadcast if it occurs before the user produces the next drawing element.
Note: We store the User code within a single object. The problem analysed below would not appear if the commands coming back from the monitor would be handled by a second object. However, this is not always possible. The actions received may influence the subsequent actions that are to be sent. The problem as addressed here is thus more general.
This system is specified by the following classes:
active class User { // user definition int figure; public: User() { figure=0 ; } // initialization constructor void broadcastCommand(int n) { // entry point offered by the printf ("%d\n",n); // user to receive its broadcast } private: @User (); // body = object's thread };active class CentralMonitor { // server definition int m; public: void accessMonitor (int m1) { // entry point where the monitor m = m1; // awaits the user calls } private: @CentralMonitor (); };CentralMonitor * S; // the pointers to the objects User * C1; // must be placed here because User * C2; // they are referenced User * C3; // in the bodies Graph * graphEnv;User::@User () { // user implementation for (;;) { select { // the user awaits a local graphEnv->get(figure); // command in parallel with S->accessMonitor (figure); || accept broadcastCommand; // the reception of its own } // part of the broadcast } }CentralMonitor::@CentralMonitor(){// monitor implementation for (;;) { accept accessMonitor; // the monitor accepts a command C1->broadcastCommand(m); // and sends it to all users C2->broadcastCommand(m); C3->broadcastCommand(m); } }void main() { graphEnv = new Graph; // instanciation of the active S = new CentralMonitor; // objects C1 = new User; C2 = new User; C3 = new User; awaitObjectTermination(); }
This program can be compiled with our sC++ compiler, an extension of the gcc of GNU. It can then be started under the control of a task level debugger. When the program above arrives in its deadlock state, the debugger shows the following picture.
The monitor can execute the application in normal mode, in accelerated mode (when the ready queue is empty, time jumps to the date of the next active object in the timer queue), or in random mode (the rendezvous and the time transitions are chosen randomly, independently of the values of the timers).
When the developer clicks on an active object in the window that lists them, the monitor displays the position where it is suspended. Not shown here is a possibility to display a history diagram that displays all past events. Thus it is easy to understand why the program cannot progress anymore. The deadlock situation is repeated below.
// user implementation User::@User () { for (;;) { select { graphEnv->get(figure); --> S->accessMonitor (figure); || accept broadcastCommand; } } } |
// monitor implementation CentralMonitor::@CentralMonitor(){ for (;;) { accept accessMonitor; --> C1->broadcastCommand(m); C2->broadcastCommand(m); C3->broadcastCommand(m); } } |
The following code avoids the deadlock. It uses labels and gotos, because they are the statements closest to states and next state jumps. Only the user code is modified to have it corresponding exactly to the automaton below.
User::@User () { // user implementation state0: select { // the user awaits a local graphEnv->get(figure); // command in parallel with goto state1; || accept broadcastCommand; // the reception of its own goto state0; // part of the broadcast } state1: select { S->accessMonitor (figure); goto state0; || accept broadcastCommand; // the reception of its own goto state1: // part of the broadcast } } }
The above code has been shown to be deadlock free.
We now define the simulation of a distributed version of the version above, and verify if there are potential deadlocks in this new version. An object simulating the RPC (remote procedure calls) can be instantiated from the following class:
active class RPC { // This class represents int m; // the whole channel between public: // the user and the monitor, void accessMonitor (int m){ // i.e., the stub, the skeleton S->accessMonitor(m); // and the TCP/IP channels } }; // Although the object has no body, it accepts // the call of accessMonitor sequentially // (atomic execution)
The user calls then the RPC object, which in turn calls the monitor. We should implement the same channel in the reverse direction, but with one direction already, the program contains a deadlock state. This situation can again be detected with the monitor. The deadlock state is shown below. The user is stuck in the middle of its RPC, which cannot terminate before the monitor accepts it. But the latter does not accepts it, because it is actually calling.
// RPC emulation active class RPC { int m; public: void accessMonitor (int m){ --> S->accessMonitor(m); } }; |
// monitor implementation CentralMonitor::@CentralMonitor(){ for (;;) { accept accessMonitor; --> C1->broadcastCommand(m); C2->broadcastCommand(m); C3->broadcastCommand(m); } } |
The solution relies on a deferred remote call. The invocation of the call is separated from the waiting of the return of its parameters. The broadcast can thus be awaited in parallel with the production of the next graphical element, the call invocation (post_accessMonitor) and the parameter return (ready_accessMonitor) respectively.
The RPC that emulates the deferred invocation is depicted below with its code.
// RPC emulationactive class RPC { int m; public: void post_accessMonitor (int m1){ m = m1; } void ready_accessMonitor (int m1){ }@RPC() { // the body supports the RPC for (;;) // protocol in parallel with accept post_accessMonitor; // the client's execution S->accessMonitor(m); accept ready_accessMonitor; } } };
Note that the server receives exactly the same call as in the previous situation. The new user code is written below. In this solution, the broadcast can occur at any time.
User::@User () { // user implementation state0: select { graphEnv->get(figure); goto state1; || accept broadcastCommand; goto state0; } state1: select { S->post_accessMonitor (figure); goto state2; || accept broadcastCommand; goto state1: } } state2: select { S->ready_accessMonitor (figure); goto state0; || accept broadcastCommand; goto state2: } } }
CORBAlization of the Application
The application can easily be completed to run in distributed mode. The sources contained in this directory have been generated by our IDL compiler. They contain all what must be added to the previous program to make it distributed. However, all stubs are generated in the same file, the skeletons in another file, etc., which is not what we want, but the problem is handled in the following. (Restrepo A.J., Petitpierre C. - CORBA Implementation: Active Objects versus Callbacks)
The structure of a complete program is shown below. The user and the central monitor have the same code as before, but the RPC emulation must now be replaced by the real CORBA calls.
Definition of the Corba interfaces
The IDL description of the interfaces of the services that can be called in the monitor and in the user, as well as the structure that is transmitted during the calls are defined below. Actually both the monitor and the user have client and server functioning. We will thus insert all interface classes used on both side in the same interface, but instantiate on each side only the needed interfaces. The post_ and ready_ versions of accessMonitor are generated automatically. Note that only the simple version appears on the server side.
IDL description:
1 struct remCommand { 2 long type ; 3 double v1X, v1Y, v2X, v2Y, r; 4 };5 interface CentralMonitor { 6 boolean connect (in string userIOR); 6 void accessMonitor (in remCommand comm); 7 };8 interface User { 9 void broadcastCommand (in remCommand comm); 10 } ;
Remember that both the user and the monitor are client and server (in the CORBA sense). The central monitor is registered in the name server, but the users pass their own object id (IOR) to the central monitor by means of the connect RPC, so that it can broadcast the data back to them.
The IDL compiler generates the following files from the definition above, assuming that it is contained in a file named editorDistr.idl. These files contain the stub and skeleton classes as well as all the commands that instantiate and initialize the various modules of the CORBA library.
editorDistr.idl |
|
interfaces and services classes defining the stubs initialization and calls of the stubs classes defining the skeletons frames of the object implementations initialization and calls of the implementations and the skeletons |
The figure below shows the structure of the user side of the application.
The code that realizes the modules displayed on the figure are given in the following. They represent the frame of the program. They are extracted from the files produced by the compiler. Not all error code is shown to avoid hiding the structure of the application. The code obtained from the analysis above can simply be inserted in this code to produce the complete application.
editorDistr_Impl.hactive class User_i : public __BASE_User{ // skeleton inheritance public: User_i(CORBA::String); ... };editorDistr_Impl.scxxUser_i::User_i(CORBA::String _key) : __BASE_User(_key) // initialization of inherited skeleton { ... } void User_i::broadcastCommand( const remCommand& comm ) { ... }; // User method called by the monitor// The main contain the initialization code void main(int argc, char **argv){ CORBA::ORB_ptr orb_ptr; // pointer to the orb services CORBA::Environment env; // error indications// Initialize the ORB env.clear(); orb_ptr = CORBA::ORB_init(argc, argv, "ior", env); if (env.exception() != 0) error(...);// Initialize a stub pointing to the naming service (name_context) CORBA::Object_ptr obj_ptr; obj_ptr = orb_ptr->resolve_initial_references((CORBA::String)"NameService"); CosNaming::NamingContext_ptr name_context; name_context = CosNaming::NamingContext::_narrow(obj_ptr);// Initialize a stub pointing to the monitor (CentralMonitor_ptr) CosNaming::Name name_CentralMonitor; name_CentralMonitor.Length(1); name_CentralMonitor[0].id = CORBA::string_copy((CORBA::String)"CentralMonitor"); CORBA::Object_ptr obj_CentralMonitor_ptr; obj_CentralMonitor_ptr = name_context->resolve(name_CentralMonitor); CentralMonitor_stub_ptr CentralMonitor_ptr; CentralMonitor_ptr = CentralMonitor_stub::_narrow(obj_CentralMonitor_ptr);// Instantiate User (my_User) CORBA::String key_User; key_User = CORBA::string_copy((CORBA::String)"User"); User_i *my_User = new User_i(key_User, 0, 0); // user instantiation// The code shows how to register an object implementation // (not needed for the user, as it registers directly in the monitor) CosNaming::Name name_User; name_User.Length(1); name_User[0].id = CORBA::string_copy(key_User); CORBA::Object_ptr UserRef; UserRef = my_User->_this(); name_context->bind(name_User, UserRef);// Registration in the monitor CORBA::String myIOR = orb_ptr->object_to_string (UserRef, env); int result = CentralMonitor_ptr->connect( myIOR ); // remote callStopMain(); exit(0); }; }
The IDL compiler has no provision to generate files according to the location of the client and server interfaces. As this application has client and server interfaces in all components, the software developer must pick the various code elements in several files. The code relating to the client interfaces are located in the file XXX_Clt_Main.scxx. The interface code related to the server is located in XXX_Impl.h, XXX_Impl.scxx and XXX_Impl_Main.scxx. We have thus developed an architecture editor that saves the structure of the application and can thus generate the frames of all components of the application.
A snapshot of a window of the architecture editor is shown below (click on the window to see it in real size).
The last part of our environment is not terminated. It will allow the modeling and the analysis of an application written in a subset of sC++ (sufficient to describe the program written in the first three design steps of the current presentation). The analyses that we will be capable of performing are the following ones:
The whole approach described above supports all libraries supported by C++, as we use the very same compiler. The use of TCP sockets or GUI interfaces is greatly simplified thanks to our interfaces based on active objects. On the other hand, we have as much support for analysis as competitive environments such as Promela/SPIN or SDT.
This situation is not dependent on sC++, and it may also happen in a program based on an event loop. For example, the program may execute a blocking remote invocation, which leaves the main stack busy until the remote function returns. If an external call is accepted at this moment and needs a resource already obtained by the program before the remote invocation, the system deadlocks.
However, in event loop programs, it is not possible to automatically identify deadlocks, as there are many ways to enable/disable the acceptance of an event: rebinding an event to another function, disable the event source (mask a window button), modify the dispatching conditions... Of course, if deadlocks are difficult to identify automatically, they are also very difficult to identify by the programmer.
Threads can be added to systems based on event loops, but it is a very difficult task, as several experiences we have heard of has shown. The problem is that the two concepts are opposite. It can even be considered that an event loop program is an inversion of a multi-thread program (Petitpierre C. An Event-Driven Programming Paradigm Compatible with OO-Programming)