Return to ING home page2.4 Design patterns

This page is part of the ING document INS-DAS-31 Design notes for UltraDAS

The UltraDAS classes follow a variety of design patterns; section 3 lists the most important ones. The patterns are stated here in order to avoid having to explain each oneb in several places.

Most of the patterns are taken from the text-book by Gamma et al; these are the famous "Gang-of-Four" patterns, and for these only the minimal explanation is listed here (actually the abstracts from the inside-covers of the book); please refer to the original book for the details. Some patterns were newly "discovered" during the UltraDAS project, and these have fuller descriptions.
 

Casual worker (UltraDAS)

Intent: to have a simple way of carrying out work in a worker thread when the work varies from case to case and may have to be aborted part-way through.

Motivation: a boss thread that starts a worker thread can easily instruct that worker by its choice of starting function and starting arguments. It is hard to change the instructions later; that requires extra synchronization between boss and worker. It is even harder to abort the work while keeping on the same worker, as the boss needs even more synchronization to detect when the worker is ready for new instructions. The Casual Worker pattern resolves this by starting a new worker thread each time new instructions are needed and firing - i.e. cancelling - the worker when the job needs to be aborted.

Consequences: there is no continuity between casual workers doing different iterations of the same job. If the job requires continuity, the boss thread must provide it in the instructions. (In this case, the Tenured Worker pattern may be better.) The worker thread must be ready to be cancelled at short notice and should respond promptly to cancellation. The boss must keep track of the thread-identifiers of the workers.

Implementation: the code to start, cancel and join with the worker thread is best kept in one object that is called from the boss thread. That is, the workers are employed by one object. For example, in udas_camera the Run objects employ Casual Workers to do readout, processing and archiving. In each of these operations, there is one blocking method on the Run object and the worker threads are started, work, return and are "payed off" (i.e. joined with) before the method returns.

Typically, the boss and worker threads communicate by sharing references to the object which employs the threads. That is, the starting argument for each worker thread is a pointer to the owning object. In this case, boss and workers must all use locking to share the object.

Casual workers need a way of signalling to the boss thread that they have finished. Pthreads condition variables are the best way. The boss can block indefinitely waiting for workers to signal, or it can block in a loop with a timeout. In the latter case, the boss typically polls for progress amongst the workers once on each pass through the loop, and reports the progress to the Facade; the boss leaves the loop when all the workersw are done.

The boss thread joins with a worker before it can start another worker on the same job. This avoid confusion from two workers fighting for the same resources.

If the boss thread is cancelled, it means that the job is aborted. The boss cancels all the workers and must join with them before leaving the method from which the workers were started. This implies that the boss can cancel the workers from a Pthreads clean-up handler.
 

Decorator (GoF)

Attach additional repsonsibilities to an object dynamically. Decorators provide a flexible alternative to sub-classing for extending functionality.
 

Facade (GoF)

Provide a unified interface to a set of interfaces in a sub-system. Facade defines a higher-level interface that makes the sub-system easier to use.
 

Fixed Plant (UltraDAS)

Intent: treat long-lived objects as a fixed part of the programme and provide an elegant way of getting references to them.

Motivation: some major objects in a programme are like the major items of plant in a physical factory; they are installed and set up before the factory starts operation, and are always present while the factory is in operation. Such an object is always available to any other object that knows about the operation of the programme and it is convenient to assume that the fixed-plant objects are always present.

Consequences: there must  be a way for the other objects to locate the Fixed Plant. A Fixed Plant object must be created when the programme starts up; it may not fail during construction. If there are initialisation operations on a Fixed Plant object that may fail, then these must be called separately after construction, and the Fixed Plant object must then maintain some internal state that tells if it is ready to operate or not. Fixed plant objects must not change their addresses between construction and destruction (they are "bolted to the factory floor").

Implementation: all the Fixed Plant objects in udas_camera also follow either the Singleton or the Planned Family pattern (q.v.). This m,eans that other objects can locate then using the selector method (q.v. Planned Family).

A Fixed Plant object typically has a start method; e.g.:

    My_plant my_plant = new_my_plant ();
    /* Construct collaborating objects here... */
    Exception x start_my_plant (my_plant);
In udas_camera, all the Fixed Plant objects are constructed and later destroyed in the main function, or in a method called directly from main. That way, the ownership of the Fixed Plant is never in doubt. In fact, the primary function of the initial object of udas_camera is to create and prepare the Fixed Plant.
 

Iterator (GoF)

Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
 

Mediator (GoF)

Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly and it lets you vary their interaction independently.
 

Planned family (UltraDAS)

Prototype (GoF)

Specify the kinds of objects to create using a protypical instance, and create new objects by copying this protoype.
 

Proxy (GoF)

Provide a surrogate or placeholder for another object to control access to it.
 

Singleton (GoF)

Ensure a class has only one instance and provide a global point of access to it.
 

Tenured worker (UltraDAS)

Intent: formalize the use of threads for periodic operations.

Motivation: some parts of the system poll to get telemetry values; this is conveniently done from a separate thread. Polling implies the same operation at each polling cycle, so there is no need to pass new instructions to the thread. In this case, the simplest usage is to have one thread for each polling loop that sleeps between polling cycles. The same thread is kept on for the lifetime of the employing object.

Applicability: the tenured worker needs to do the the same job at each cycle. If the job changes, such that the worker needs to communicate with some boss thread at each cycle, then it may be simpler to use the Casual Worker pattern (q.v.) and to pass the new instructions to the Casual Worker thread as it starts up. If the work is not simple polling, there might be a need to abort a cycle part-way through; this would be harder with a Tenured Worker than with a Casual Worker. Finally, the Tenured Worker needs to be able to recover from exceptions with minimal help.

Consequences: the object employing the Tenured Worker becomes an active object with time-dependent behaviour. Once the worker thread starts, there is n o way of stopping it short of destroying the employing object.

Implementation: in UltraDAS, objects generally have initialization methods that are called explicitly some time after construction. A class employing tenured workers would be expected to have methods like these

    new_boss : Boss
    start_boss (this: Boss, instructions_for_worker) : Exception
    end_boss (this: Boss) : Boss
in which start_boss launches the worker thread and end_boss cancels it and joins with it. The worker thread could be made detached, in which case end_boss would only need to cancel it. However, a detached worker would only go away asynchronously, and this might cause a race condtion in a restart sequence like
    boss = end_boss (boss);
    boss = new_boss ();
    Exception x = start_boss (boss,<whatever>);
where the old worker could still be holding resources that the new worker needs to get started. Joining with the worker threads is the safe way to be sure that they have finished.

Clearly, the worker thread needs to release all its resources when it is cancelled. It can either do this with Pthreads clean-up handlers, or can disable cancellation during critcal sections of code (providing those sections are short and have no places where the thread can block). This technique:

    LOCKL (this->lock);
    this->thingy = get_thingy_after_long_wait (some_object);
    UNLOCKL (this->lock);
will generally work but is weaker than
    local_thingy = get_thingy_after_long_wait (some_object);
    LOCK (this->lock);
    this->thingy = local_thingy;
    UNLOCK (this->lock);
because the former code has more chance of deadlock. Note that there is no cancellation point inside the locked area of the latter code.
 

Tradable object (UltraDAS)

Intent: formalise the transfer of ownership an object from one aggregate object to another.

Motivation: an object may be gradually built inside another object and then dispensed to another parent for further use. An example is the construction and elaboration of a Mosaic inside a Pixel-stream and its subsequent transfer to a Readout object owned by a Run object. If ownership of objects is formalized, as it is in UltraDAS, then the transfer needs to be formalized as well.

Consequences: the objects transferring the tradable object have to recognise "before trade" and "after trade" as distinct states. The object traded should not retain any reference to its old owner; that assocation should only be navigible from the owner to the owned object.

Implementation: the transfer needs to happen as an atomic operation. Code like

    new_owner->thingy = transfer_thingy (old_owner);
(which is called from inside a method of new_owner) is preferable to
    new_owner->thingy = get_thingy (old_owner);
    relinquish_thingy (old_owner);
since the latter leaves both owners sharing title to the Thingy between the two calls; this is clearly dangerous if the owners are MT-safe and could accept method calls from other threads. Furthermore, the transfer_thingy call must prevent any use of the Thingy inside old_owner during the transfer. New_owner should disable thread cancellation during the transfer so that there is no chance to lose the Thingy.