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.
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.
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.
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) : Bossin 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.
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.