ACE: Framework Design Rules

孙化
2023-12-01

Original URL: http://www.cs.wustl.edu/~schmidt/rules.html


Framework Design Rules

This document describes design rules to follow while developing and using object-oriented frameworks for communication systems.

Table of Contents

1.Introduction
2.Programming Principles
3.Compiler Concessions
4.Configuration Concerns
5.Initialization Issues
6.Pattern Practices
7.Multi-threaded Matters
8.Real-time Rules
9.International Intents
10.Conclusions

1. Introduction

The OO application framework design rules described below have been derived by re-engineering the design, implementation, and proper use of the ACE C++ framework. ACE is an object-oriented (OO) framework that implements many core design patterns for concurrent communication software. ACE provides a rich set of reusable C++ wrappers and framework components that perform common communication software tasks across a range of operating system platforms.


2. Programming Principles

This section describes coding practices that directly result from practical framework design issues, including maintainability and reusability.


#guard header files

Rule
Always guard against multiple inclusion of header files.
Example
Suppose we have files a.h, b.h, and c.h which declare classes A, B and C, respectively. Further suppose that B and C both inherit from A, and that C contains a reference to B. Hence, b.h includes a.h and that c.h includes both a.h and b.h.
Rationale
Without guarding against multiple inclusion, c.h from the example above will contain two declarations for class A.
Applicability
Any header file.
Consequences
Prevents multiple declarations. Requires additional code to be written. However, this can be "boiler-plated" by a suitable development environment or program editor.
Exceptions
There are no exceptions.
Known Uses
Applies to every header file.
Enforcement
Can be automated.

Avoid global functions

Rule
Avoid the use of global functions. If functions are required, place them within a namespace or nested in a class.
Example
Integrating a framework like ACE with third party frameworks like X Windows and other class libraries like STL or Tools.h++ require callback functions to be defined and passed into them. For instance, thread creation functions require a C-style function:
void *thread_entrypoint (void *arg)
{
// ...
}

// ...
thr_create (0, 0, thread_entrypoint, param, 0, &tid);
However, defining functions like my_callback at global scope can pollute the namespace. Namespace pollution makes it hard to integrate the various frameworks and class libraries. Therefore, all global functions in a framework should be preceded with a unique prefix. One way to do this is as follows:
void *ace_thread_entrypoint (void *arg)
{
// ...
}

// ...
thr_create (0, 0, ace_thread_entrypoint, param, 0, &tid);
Often, a better way to structure this code is to scope it as a static method within a class, as follows:
class ACE
{
static void *thread_entrypoint (void *arg)
{
// ...
}
};

// ...
thr_create (0, 0, ACE::thread_entrypoint, param, 0, &tid);
This solution is more abstract, but may cause problems with certain C++ compilers (such as the IBM MVS C++ compiler) that don't allow static member functions to be used as parameters for functions that expect C functions (such as thr_create or signal).

If a compiler supports namespaces, an even better way to structure this code is to scope it within a namespace, as follows:

namespace ACE
{
void *thread_entrypoint (void *arg)
{
// ...
}
}

// ...
thr_create (0, 0, ACE::thread_entrypoint, param, 0, &tid);
However, many older C++ compilers don't support namespaces yet, so this approach is less portable.
Rationale
Protecting global functions with namespace or class scoping, minimizes the possibility of colliding with user code.
Applicability
This rule should be applied whenever a C++ function is required.
Consequences
If the compiler lacks support for namespace, then scoping inside a class is acceptable but less convenient.
Exceptions
None
Known Uses
ACE precedes its handful of global functions with the ace_ prefix. Moreover, it places most stand-alone functions as static methods within the ACE class.
Enforcement
Can be automated.

Respect user namespace

Rule
Avoid polluting the global namespace.
Example
Rationale
This rule is a generalization of the Avoid global functions. It extends the application of the rule to global variables as well as functions.
Applicability
Consequences
Exceptions
Known Uses
Enforcement
Can be automated.

Use explicit destructor with placement new

Rule
All uses of the placement new operator must be accompanied by a corresponding explicit destructor call.
Example
Consider the following code fragment.
class A *arr = Allocator::malloc (MAX * sizeof (class A));
for (int i = 0; i < MAX; i++)
new (arr + i) A (i);
// ...
Allocator::free (arr);
Rationale
If class A dynamically allocates memory for itself, the example above results in memory being leaked. In order to reclaim the memory, the destructor should be explicitly called.
for (int i = 0; i < MAX; i++)
(arr + i)->A::~A (); // arr[i].A::~A (); is equivalent,
Note that it is incorrect to attempt to use delete on any part of arr, since the the memory was not allocated with new.
Applicability
This still applies (but in reverse) if placement new is applied to an existing fully initialized object. That is, the destructor should be called explicitly first before placement new is applied.
Consequences
Faithfully applying this rule will lead to fewer problems with memory leaking. However, programmers may decide not to use general memory allocators to avoid the complexity of remembering to call destructors explicitly.
Exceptions
There are no exceptions.
Known Uses
In ACE, classes that allow for parameterizable allocators follow this rule.
Enforcement
Automatic enforcement imperfect without data-flow analysis.

Avoid long pointer dereferencing chains

Rule
Avoid long chains of pointer dereference operators.
Example
Avoid lines of code that resemble a ()->b ()->c ()->d ()->e ();. Since actual objects names are usually longer than a single character, one can imagine such a chain being difficult to read.

In addition, if the line of code is executed in a performance-critical section of the program many C++ compilers will not optimize this.

Rationale
The primary concern is that of code readability and maintainability. When debugging, the programmer is faced with tracing through number methods to understand the behavior of the code fragment. Efficiency is a secondary, but relevant, issue.

Adherence to this rule can lead to framework designs that are more cohesive, and easier to understand.

Applicability
Everywhere.
Consequences
Makes method invocations easier to follow. A performance speedup may result by explicitly creating intermediate temporary pointers.

May result in programmers manually creating temporaries before derefencing the next method call. However, these can often be optimized away by the compiler.

Exceptions
None.
Known Uses
ACE, TAO and JAWS.
Enforcement
Can be automated.

Avoid making direct system calls

Rule
Framework users should always use wrappers instead of directly making system calls.
Example
Suppose a framework developer has need to create a temporary file with a name that can be passed to another process ( i.e., the Standard C function tmpfile() is insufficient). In UNIX, the developer may be tempted to use the mktemp() system call, which creates a temporary file name that thus far does not exist. However, this system call is not available on all platforms ( e.g., VxWorks and OSF1). Rather than making a direct system call, a wrapper should be used instead. For example,
namespace ACE_OS {
char *
mktemp (char *s) {
#if defined (ACE_LACKS_MKTEMP)
// ... framework provided emulation
#else
return ::mktemp (s);
#endif /* ACE_LACKS_MKTEMP */
}
}
Thus, a temporary file can now be created with a call to ACE_OS::mktemp().
Rationale
This rule allows the framework to flexibly handle portability issues by emulating functionality when needed. This allows developers to program to a common API.
Applicability
Consequences
Exceptions
Known Uses
Enforcement
Can be automated.

Use consistent error-handling

Rule
The framework should employ and maintain a consistent error-handling interface.
Example
There are many styles of error-handling. Exceptions are beginning to become ubiquitous among C++ compiler implementations, but the quality of implementation across different vendors remains inconsistent. Another technique uses a error code return value, commonly used by system calls and synchronous method calls. Finally, classes that provide asynchronous methods may handle errors by providing a status method that reports the state of the asynchronous task.

If a framework design lacks consistent error-handling, then semantically similar objects (such as containers) may present the framework user with different mechanisms for detecting error conditions. Which may be a potential source of confusion.

Rationale
Employing and deploying a consistent error-handling interface in the design of a framework can reduce the cost of maintenance, and raise its readability, and usability.
Applicability
Any exported class with methods whose execution may result in an error.
Consequences
It is admittedly difficult to choose a single error-handling interface that is flexible enough to handle all possible problems that may arise.
Exceptions
None.
Known Uses
JAWS Filecache and IO.
Enforcement
Code review.

Implement dump() methods

Rule
Instrument a dump() method in every object.
Example
Suppose we are following the Allow for run-time tracing rule. It is often the case that the state of the object needs to be made known to create meaningful trace output.
Rationale
Following this design principle allows for easy inspection of the state of any object.
Applicability
Every object which maintains state that may need to be inspected at run-time.
Consequences
Enables run-time tracing. However, it may violate encapsulation unless the programmer has diligently provided accessor methods to the object's state.
Exceptions
Stateless objects.
Known Uses
ACE.
Enforcement
Can be automated.

Use abstractions for file descriptors

Rule
Abstract away different representations of handles / file descriptors.
Example
Windows NT descriptors are represented as pointers, while in UNIX, they are represented as integers. If the framework is to be portable across both platforms, these differing representations need to be reconciled. In ACE, this reconcilation is achieved with ACE_HANDLE.
#if defined (ACE_WIN32)
typedef HANDLE ACE_HANDLE;
#else
typedef int ACE_HANDLE;
#endif /* ACE_WIN32 */
Here, HANDLE is the Win32 type name given for file descriptors, whose underlying representation is a void *.
Rationale
By applying an abstraction such as ACE_HANDLE to the representation of descriptors for both Windows NT and UNIX, framework developers can be more confident that the code can be ported to either platform with minimal disruption to application logic.
Applicability
Framework that performs operations on descriptors on multiple platforms.
Consequences
This technique may create a mandate that framework users program to the interface of the most exotic platform. For example, Windows NT uses a separate pointer type to represent network handles than from file handles, where as in UNIX they are both integers. In order to propogate this design rule, a new abstraction would be created that would become a network handle in Windows NT, and an integer in UNIX. This means framework users would be programming to the notion of separate handle types for file and network handles, even though they are the same type in UNIX.
Exceptions
Single platform environments.
Known Uses
ACE.
Enforcement
Can be automated.

Allow for run-time tracing

Rule
Instrument framework methods to allow for run-time tracing.
Example
When developing applications, programmers often find the need to annotate portions of their program with print statements so as to be able to determine what state a program is in at the point of a failure. However, bare print statements are inconvenient after debugging has been completed, since all the trace statements have to be removed manually. To solve this problem, many developers employ a macro wrapper that contains the print statement, and can be selectively removed by redefining the macro to be blank.

The ACE programming framework provides such tracing capabilities through its ACE_DEBUG macro. This flexible facility has the ability to send the trace messages to the screen, a file, or to a log server.

Rationale
Incorporating tracing mechanisms into the framework design provide aid to the debugging process of framework applications.
Applicability
Consequences
When using macros that may be selectively removed, care has to be taken to not include expressions with side-effects to be taken as arguments into the macro.
Exceptions
Known Uses
Enforcement
Code review.

Check all returned values

Rule
Either check or return all returned values from system functions and framework method calls.
Example
This rule is perhaps obvious, but it is quite an easy point to overlook. Developers often prototype code that they do not believe will be included in any actual product. This code can be, undestandably, sloppily written. However, when the prototype is shown to be useful, they often become included into the production code base. Perhaps the most common mistake is to not verify the result of a call to new.
Rationale
This can be considered an extension to the Use consistent error-handling rule. A program should consistently never assume all calls are successful.
Applicability
Consequences
Exceptions
Known Uses
Enforcement
Can be automated.

Qualify methods not in scope

Rule
Qualify references to base class methods and data. I.e., anything outside the scope of the class.
Example
Rationale
Without a qualifying references, it is often not obvious to the code reviewer which function is being invoked. Qualifying these references increases the maintainability of the code.
Applicability
Consequences
Exceptions
Known Uses
Enforcement
Can be automated.

Avoid data members in base classes

Rule
Base classes should not store information about derived classes.
Example
Consider in ACE the Event_Handler. This abstract class is used as the base class for objects to be managed by a Reactor. Since the Reactor is a pattern based on the UNIX select() system call, an Event_Handler is associated with a HANDLE. However, storing the HANDLE associated with the Event_Handler in the abstract base class would be a mistake. The HANDLE is needed by the derived classes in order to perform their low-level actions, such as reading from and writing to the descriptor. The only way to ensure access would be to make the HANDLE data member public, or to provide public accessors to the data. However, a simpler solution is to create a virtual accessor method in the abstract class.
Rationale
Instead, the base class should provide virtual accessor methods to the information.
Applicability
Consequences
Exceptions
Known Uses
Enforcement
Can be automated.

Implement open()/close() methods

Rule
Use open() methods to initialize objects rather than constructors. Likewise, use close() methods to finalize objects rather than destructors.
Example
Suppose we are creating an object that with many different constructors. It is tedious to implement each constructor if many of the initializations are similar. The process can be simplified by implementing an open() method which performs the common initializations.

Moreover, as indicated in the Initialize on first use rule, it is often useful to create static objects but to postpone their initialization (until the existence and initialization of their dependents can be verified). The existence of an open() method provides a mechanism for allowing this to take place.

For destructors, consider the Use unguarded destructors rule. However, if shared data has been dynamically allocated, then it becomes necessary to serialize the contexts that attempt to release the allocated data. The presence of a close() method provides a callable method for which this can be done and allows the destructor to remain guard free.

Rationale
Creation and initialization are orthogonal issues. Likewise, destruction and finalization are orthogonal.
Applicability
Consequences
Exceptions
Known Uses
Enforcement
Can be automated.

3. Compiler Concessions

This section presents rules that allow the framework design to flexibly cope the wide range of C++ compilers and their various levels of compliance with the C++ Standard.


Avoid using inline directly

Rule
Avoid using the inline keyword directly in C++ code. Instead, use a macro that can be automatically toggled to inline or not inline. Separate the inlines into their own implementation files, i.e., do not put inlined functions in header files.
Example
C++ inline functions are often useful to eliminate the invocation overhead of small, commonly used C++ methods at run-time. The following is a common way of writing this code:
class ACE_SOCK_IO
{
public:
ssize_t send (const void *buf, int n) const
{
// Call underlying OS function to send &ltbuf&gt.
return ::write (this->get_handle (), buf, buf_size);
}

// ...
};
Although this approach works, it is inflexible since it hard-codes the use of inlining into the framework and makes it difficult to separate the interface of a class from the implementations of its methods.

It is often useful, however, to disable inlining when debugging a framework or debugging applications build using a framework for the following reasons:

  • Many debuggers get confused when they encounter inlined code. This makes it hard to step through the execution sequence of a program in the debugger.

  • If the code is always inlined, any changes to method implementations will cause all dependent object files to be recompiled.

  • Separating the interface from the implementation makes it easier to understand the documented semantics of a class.

Therefore, it is often more useful to structure inlined C++ code as follows:
// System-wide include file OS.h

#if defined (__ACE_INLINE__)
#define ACE_INLINE inline
#else
#define ACE_INLINE
#endif /* __ACE_INLINE__ */

// Header file SOCK_IO.h

#include "ace/OS.h"

class ACE_SOCK_IO
{
public:
ssize_t send (const void *buf, int n) const;
// ...
};

#if defined (__ACE_INLINE__)
#include "ace/SOCK_IO.i"
#endif /* __ACE_INLINE__ */

// Include file SOCK_IO.i

ACE_INLINE
ssize_t
ACE_SOCK_IO::send (const void *buf, int n) const
{
// Call underlying OS function to send &ltbuf&gt.
return ::write (this->get_handle (), buf, buf_size);
}

// Implementation file SOCK_IO.cpp

#if !defined (__ACE_INLINE__)
#include "ace/SOCK_IO.i"
#endif /* __ACE_INLINE__ */
Rationale
By using a macro like ACE_INLINE in place of the inline keyword, the same source file can be used for building both debug and optimized versions of the framework. For debug versions, the macro can expand to an empty string, and the source compiled separately. For optimized versions, the macro can expand to inline, and be #include'd by the header file. This separation of concerns makes it easy to switch between debug and optimized configurations.

If the inlined code has not been separated, then the change causes all dependent objects to be recompiled. In addition, by completely separating method implementation from method interfaces, header files can more clearly document the semantics of each method.

Applicability
Use this rule whenever you write C++ methods that should be easy to debug, as well as optimized.
Consequences
Editors that support source code parsing (for correct syntax and color highlighting) may get confused and parse the macro incorrectly. In addition, creating separate *.i files can increase the number of files that programmers must understand.
Exceptions
If a function is very short, will never fail, and should always be inlined, then it may make sense to use the inline keyword directly in the *.i file, as follows:
// Include file SOCK_IO.i

inline
ssize_t
send (const void *buf, int n) const
{
return ::write (this->get_handle (), buf, buf_size);
};
Note that it is still a good idea to have a separate include file, rather than including the method implementation in the class definition to enhance clarity and ensure flexibility for future changes.
Known Uses
The ACE framework uses this rule extensively.
Enforcement
Can be automated.

Separate templates from non-templates

Rule
Template classes should not be mixed with non-template classes.
Example
Suppose a framework developer has created a template class, ACE_Task, which provides a general interface for active objects. The developer follows the Use a non-template class as base in order to allow references to this class to be passed as an argument to methods of other classes, such as the ACE_Thread_Manager. Then, the developer may be tempted to define the template and base at their point of use.
#ifndef TASK_H
#define TASK_H

class ACE_Task_Base
{ // ...
};

template
class ACE_Task : public ACE_Task_Base
{ // ...
};

#endif
However, upon compiling applications based on the framework, the developer finds the linker to complain about multiple definitions for ACE_Task_Base.
Rationale
This is a portability issue. Some compilers require that templates be in some accessible header file in order for the linker to resolve template instantiations. However, if mixed with non-template classes, multiple definitions may result.
Applicability
Consequences
Exceptions
Known Uses
Enforcement
Can be automated.

Use traits in templates

Rule
Use traits to merge multiple (related) template arguments.
Example
Consider the following example from the ACE framework. ACE defines an abstraction that can be used to parameterize allocation strategies applied by classes that need dynamic memory management. This abstraction is called ACE_Allocator. In order to facilitate the deployment of a variety of allocation strategies ACE provides a template called ACE_Malloc. This template is parameterized by MEMORY_POOL type, and by the locking strategy. Thus, enabling the allocation to be drawn from a static, dynamic, and/or persistent MEMORY_POOL. The type of MEMORY_POOL is typedef'd withing the ACE_Malloc class to create a trait. This trait is used when ACE_Malloc is specialized and applied to the ACE_Allocator_Adapter (an adapter template that subclasses from ACE_Allocator and delegates to a memory allocation strategy, such as ACE_Malloc). Without the use of the trait, MEMORY_POOL would have to be passed as a paramter the ACE_Allocator_Adapter.
Rationale
By employing traits, templates achieve greater consistency, are more reliable, and are easier to use.
Applicability
Consequences
Exceptions
Known Uses
Enforcement
Automation is probable.

Use a non-template class as base

Rule
Try to have a base class that is not a template, from which specialized classes can be derived that may have template parameters.
Example
Rationale
This rule also reduces the number of template parameters needed by allowing a non-template class provide the interface by which the template must follow. Thus, the instance of the template may be referenced though its base. See Use traits in templates.
Applicability
Consequences
Exceptions
Known Uses
Enforcement
Can be automated.

#guard template source files

Rule
Templates may require source. Hence, template source files may need to be guarded against multiple inclusion too.
Example
Suppose we are following the Avoid using inline directly design rule, and have separated implementation code from the object declaration. Furthermore, the implementation code that can be inlined is held in their own separate file. The table below depicts the situation.
class.h
class.i
#if !defined (CLASS_H)
#define CLASS_H

#include "ace/OS.h"

class A
{
public:
ACE_INLINE A (void);
~A (void);
};

#if defined (__ACE_INLINE__)
#include "class.i"
#endif /* __ACE_INLINE__ */

#endif /* CLASS_H */
#if !defined (CLASS_I)
#define CLASS_I
A::A (void)
{
// ...
}
#endif /* CLASS_I */
class.cpp
#include "class.h"

#if !defined (__ACE_INLINE__)
#include "class.i"
#endif /* ! __ACE_INLINE__ */

A::~A (void)
{
// ...
}
Rationale
See #guard header files.
Applicability
All implementations of templates.
Consequences
Exceptions
None.
Known Uses
All template classes and functions in ACE follow this design rule.
Enforcement
Can be automated.

4. Configuration Concerns

The issues addressed by these guidelines impact the design of frameworks with regard to how to ease porting tasks among different compiler, OS and hardware platforms.


#guard against compiler quirks

Rule
Guard against quirky compiler implementations through #define abstractions (whereever possible).
Example
Some compilers require special keywords in order to make objects exportable in a dynamically linked library (MSVC++). Other compilers require explicit template instantiation. Different compilers may have different mechanisms to explicitly instantiate templates ( e.g., #pragma instantiate, or via a special declaration form).
Rationale
The special keywords are only needed when the header file is used to build the library. When the user #include's the header file, the keyword should be ommitted. The use of a #define makes it possible to reuse the same header file for both framework developer and framework user.
Applicability
Files that are compiled on multiple platforms.
Consequences
Unless carefully done, may complicate things for code parsing tools.
Exceptions
Single platform environments.
Known Uses
ACE, JAWS, TAO.
Enforcement
Code review.

Specify a platform by its features

Rule
Don't use conditional compilation based on compiler/OS/hardware platform, but upon available features.
Example
Rationale
The conditional compilation preprocessing lines become descriptive. It is conceptually easier to port the framework to a new platform, since it only requires that the platform be described in terms of the features that are available.

The alternative would be identify every conditional pre-processing line in the source and determine whether or not the platform under consideration requires to be added to the conditional or not.

Applicability
Any framework where portability is an issue.
Consequences
Eases porting task. If a new platform has a feature that none of the previous platforms have, then it can result in feature descriptions that are both positive (ACE_HAS_FEATURE) and negative (ACE_LACKS_FEATURE).
Exceptions
If portability is not an issue, rule does not apply.
Known Uses
ACE, JAWS, TAO.
Enforcement
Can possibly be automated.

Use a config.h file

Rule
Centralize portability #ifdefs in a single place to ease portability maintainence.
Example
Suppose that the code for application App was written for UNIX, and is then to be ported to Windows NT, and it consists of many source files. It is a bad idea to visit each source file and add
#ifdefs UNIX
// ...
#else
// ...
#endif
Everywhere the code needs to change.
Rationale
The next platform that App needs to be ported to will require a re-visitation to each source file. If instead the incongruities are reconciled in a single place, then portability is enhanced by reducing the number of files that require changing.
Applicability
Any framework that depends upon portability.
Consequences
Ultimately, this leads to the creation of wrappers.
Exceptions
If the framework is never intended to be ported elsewhere, this rule does not apply.
Known Uses
ACE.
Enforcement
Might be automated.

5. Initialization Issues

Initialialization issues arise naturally in frameworks, since there are various objects for which only one instance should ever exist, known as singletons. Often, singleton classes will employ the use of static declarations for its own data members. This section warns about such practices.


Initialize on first use

Rule
Avoid the use of static/global objects whose constructors must be run in order to initialize the objects correctly. Instead, use Singletons that apply the Double-checked locking optimzation pattern.
Example
Suppose there is a static class of type A, that contains a data member that is a class of type B. Further, suppose that B contains static data.
Rationale
Since C++ does not guarantee the order of static initializations, unexpected results are possible if A depends upon the static values in B in its initialization.
Applicability
Use this rule whenever you develop class libraries or frameworks that require the use of objects that are logically global or static.
Consequences
It can be tedious to remove all uses of static and global objects from a large software system.
Exceptions
Static/global objects can be used correctly if all their initial values are 0 and/or they are simple pointers.
Known Uses
The ACE framework contains no unsafe static/global objects. Instead, it uses a single global Object Manager to control the lifetime of static/global objects.
Enforcement
Automation probable.

Avoid creating statics

Rule
Do not make static objects whose correctness depends on constructors being called ( i.e., if 0 initialization is not sufficient).
Example
Rationale
See nostatics.
Applicability
Consequences
Exceptions
None.
Known Uses
Enforcement
Automation probable.

Ensure Singleton Destruction

Rule
Ensure Singleton destruction by providing hooks that delete Singletons before a process exits.
Example
Consider an editor application that uses a filemanager object to remember which files are opened in memory. Suppose this object is a singleton. The editor may need to maintain session information, so that the next editor invocation will have all the same files open as the last.
Rationale
An ideal time for the information to be recorded is before the editor actually exits. Since the information is contained in the filemanager, the destructor of the filemanager can record the necessary information so that the next invocation can open the files that had been open before.
Applicability
Any occurrence of a singleton object.
Consequences
Requires programmer to consider the possibility of singleton destruction.
Exceptions
None.
Known Uses
JAWS Filecache.
Enforcement
Automatic enforcement probable.

6. Pattern Practices

The design rules here encourage the use of particularly appropriate patterns that lend themselves to providing identifiable framework idioms. In general, these patterns particularly strengthen the skeleton nature of the framework while increasing the flexibility available to the application developer.


Strategize Memory Allocation

Rule
Allow customization of memory allocation on a per-object or per-thread basis.
Example
Consider a real-time Object Request Broker, such as TAO. To reduce lock contention and avoid priority inversion, it is important to localize the scope of dynamic memory operations to memory pools allocated in thread-specific storage. Likewise, for medical imaging systems, it is often useful to allocate memory out of memory pools residing in shared memory to reduce data copying between separate image processing processes.

One way to control of memory allocation/deallocation in C++ is to overload operators new and delete either globally or in a class-specific manner. However, this design is too general and can cause many unrelated parts of the system to behave incorrectly.

A more flexible way of controlling memory allocation/deallocation in C++ is to define an Allocator component that can be parameterized in various ways, such as on a per-object or per-thread basis. For example, rather than saying:


Image *create_image (bool use_shared_memory)
{
Image *image;

if (use_shared_memory)
{
// ... mmap() file, obtain a pointer to shared memory
// region, etc.
image = new (shared_memory_pointer) Image (use_shared_memory);
}
else // use local memory.
image = new Image (use_shared_memory);

return image;
}
Developers can write

Image *create_image (ACE_Allocator *allocator)
{
char *buf = allocator.malloc (sizeof Image);
return new (buf) Image (allocator);
}
This design makes it possible to replace memory management strategies wholesale without affecting the application interfaces. In addition, it greatly reduces the effort required to keep track of which memory management strategies are associated with each object or thread.

Rationale
If memory allocation is a parameterized strategy, then it is straightforward to extend objects or threads to use different memory management policies transparently.
Applicability
This rule should be used whenever a framework requires fine-grained control over memory allocation and deallocation.
Consequences
Allows allocation strategies to be parameterizable. Requires generic allocation interfaces. Also, may require each object and their subobjects to be parameterized by a memory management strategy.
Exceptions
Known Uses
ACE, JAWS, and TAO use parameterized memory management strategies extensively.
Enforcement
Code review.

Apply the strategy pattern

Rule
Apply the strategy pattern to factor out common sources of variation in a component.
Example
Rationale
A framework should provide enough structure as to form a skeleton application. Yet, it should provide enough flexibility so that application developers can do their job. Liberal application of the strategy pattern helps achieve this balance of structure and flexibility.
Applicability
Consequences
Exceptions
Known Uses
Enforcement
Automation unlikely.

Create a wrapper for similar functions

Rule
Define wrappers around clusters of functionality that are semantically the same but may have accidental incompatibilities. E.g., semaphores, readers/writer locks, mutex + condition variables, which can be used by threads, processes, that may or may not reside on the same machine.
Example
Rationale
Application of the adapter pattern. Eases usability of the framework by providing users with identical programming interfaces for similar OS services.
Applicability
Consequences
Exceptions
Known Uses
Enforcement
Automation unlikely.

Parameterize components by wrappers

Rule
Define generic components that can be parameterized by the wrappers defined above (in the previous point).
Example
Rationale
This enables components to keep their logic constant across different platforms.
Applicability
Consequences
Exceptions
Known Uses
Enforcement
Automation unlikely.

Implement iterators

Rule
In searchable containers, separate traversal logic from item operations.
Example
Consider a container class that is used to maintain a dynamic priority queue of events. There are a number of operations which require examining all the items in the container. For instance, the insertion of a new event may require that the priorities of all events be re-evaluated. Without an iterator on the container, the priority queue is required to understand the underlying representation of the container class in order to implement the priority re-evaluator.
Rationale
Providing a method to iterate over items in a container insulates the user from the underlying representation of the container.
Applicability
Any searchable container.
Consequences
Permits entier contents of container to be inspected. Enables a dump() method to be more easily implemented.
Exceptions
None.
Known Uses
ACE containers.
Enforcement
Automation unlikely.

Separate creation from use

Rule
Separate the creation of an object from its use.
Example
Suppose we are implementing a client/server system. Consider the server task of establishing a passive connection entry point which clients can use to negotiate a commnication channel to the server. There are details involved in this process, table lookups and structure initialization, that must be done before the passive connection is actually created.
Rationale
The abstraction is useful in situations where the logic behind the use of an object remains constant, but initialization of the object may be different across platforms.
Applicability
Consequences
Exceptions
Known Uses
In ACE, this is applied to the Acceptor and Connector, as well as the Service Handler.
Exceptions
Automation unlikely.

7. Multi-threaded Matters

The use of these guidelines help framework designs avoid the common pitfalls present in the development of concurrent systems. Namely, race conditions and deadlocks. Also included are design rules that help avoid contention.


Make all methods reentrant

Rule
In multi-threaded objects, all methods should be re-entrant.
Example
Rationale
If a method does not modify shared data, then concurrency is possible. Otherwise, mutual exclusion is required. Eitherway, race conditions are avoided.
Applicability
Consequences
Exceptions
Known Uses
Enforcement
Automation unlikely.

Avoid using sentinels

Rule
Do not use sentinels or any other shared state that must be updated while searching a container that uses readers/writer locks.
Example
Containers are abstract data types (ADTs) that provide operations like bind/unbind, which insert and remove items in a container and find, which searches for an item in a container. Common implementations of these containers use linked data structures to implement hash tables, lists, and trees.

In non-threaded applications, it is often possible to speed up container operations by using a sentinel. For instance, an implementation of a map that uses a hash table with ``bucket chaining'' to resolve collisions might be implemented as follows:

template 
   
   
    
    
class ACE_Hash_Map_Manager
{
public:
ACE_Hash_Map_Manager (size_t table_size);
// Constructor.

int find (EXT_ID ext_id, INT_ID &int_id);
// Returns the if is in the hash table.

// ...

private:
struct ACE_Hash_Map_Manager_Entry
{
EXT_ID ext_id_;
// External identify used as a key to find
// the internal identifer.

INT_ID ext_id_;
// Internal identifier holds the value.

ACE_Hash_Map_Manager_Entry *next_;
// Points to the next entry in the overflow bucket.
};

ACE_Hash_Map_Manager_Entry **hash_table_;
// Array of pointers to the linked list of entries.

ACE_Hash_Map_Manager_Entry *sentinel_;
// Sentinel node.

LOCK lock_;
// Synchronization strategy.
};
In a sentinel-based hash-table implementation, the constructor typically allocates a sentinel and initializes all the pointers in hash table to reference the sentinel, as follows:
ACE_Hash_Map_Manager
Once the sentinel has been initialized, the find method can be optimized as follows:
int
ACE_Hash_Map_Manager
Although this code will work correctly if given a ``NULL'' lock (such as ACE_Null_Mutex) it will fail if given a readers/writer lock (such as ACE_RW_Mutex). The problem is that a readers/writer lock only works correctly if the region of code it protects does not modify the state of the object. In this case, the sentinel_ maintains state that is shared by all threads that access the hash table. Therefore, the find can fail since there are race conditions if multiple threads concurrently update the sentinel_.
Rationale
Avoiding the use of sentinels in find operations reduces contention by removing the critical section around the storage of search value in the sentinel. In addition, it also avoids subtle race conditions that arise if the synchronization strategies are configured into parameterized types.
Applicability
Any searchable container class that uses readers/writer locks.
Consequences
One drawback to using this rule is that containers whose synchronization strategy is configured via parameterized types may not run as optimially when configured with "null" locking strategies.
Exceptions
Contains that will only be used in single-threaded applications need not follow this rule.
Known Uses
All ACE containers that support find operations and can be configured with readers/writer locks do not use sentinels.
Enforcement
Automation unlikely.

Use unguarded destructors

Rule
The state of an object does not need to be guarded in destructors.
Example
Suppose class A has destructor A::~A. A::~A should only be performing idempotent actions that release resources.
Rationale
There is no purpose in creating unnecessary resource contention. If class A is holding a shared resource, then the resource itself should be guarded, not A.
Applicability
Consequences
Destructors should not directly modify static memory if the action is not idempotent.
Exceptions
Known Uses
Enforcement
Automation unlikely.

Guard open()/close() methods

Rule
Guard against multiple open() and close() calls, for idempotency.
Example
Assume the we have followed the open design rule so that we have separated object initialization from object creation. Suppose we have such an object A which happens to dynamically allocate memory upon initialization. Furthermore, A is referenced from multiple contexts ( e.g., threads of control). It may happen that each context may attempt to initialize A if both concurrently view A as uninitialized. If A is unguarded, then a memory leak may result. Similar problems may occur in the finalization of A.
Rationale
The presence of guards would avoid race conditions. The use of the double check locking pattern can be used to achieve idempotent initialization and finalization.
Applicability
Every object that may be multiply referenced.
Consequences
Allows initilialization and finalization to be idempotent.
Exceptions
If object is never referenced by more than once, rule does not apply.
Known Uses
ACE.
Enforcement
Code review.

Use unguarded protected/private methods

Rule
In classes that require locking, have public methods acquire the locks and call protected/private methods that do not acquire them.
Example
Consider a multi-threaded searchable container class. Typical methods include find, insert, and remove. Suppose, however, that the user requires the ability to atomically update an entry. Since the given remove and insert methods are themselves atomic, they cannot be used by update (otherwise, deadlock). Thus, adding an update method to the container class requires copying and pasting code from both remove and insert.

This problem is resolved by providing private internal methods that perform the actual actions of finding, inserting and removing. These internal methods assume that locks are already held when they are called, and so do not require locking themselves. If these methods are called find_i, insert_i, and remove_i, then update can easily be implemented by applying remove_i followed by insert_i.

Rationale
Promotes reuse. Eases programming effort.
Applicability
Any data structure that requires synchronization in a multi-threaded framework.
Consequences
Exceptions
Single threaded environments.
Known Uses
ACE containers, JAWS Filecache.
Enforcement
Code review.

Wait for spawned threads/processes

Rule
Always remember to wait for the termination of spawned threads and/or processes in the main the thread before exiting.
Example
Rationale
Not waiting may cause the system to lose resources that have not been properly released. For example, if threads hold references to shared memory resources (such as semaphore variables), these may not get released back to the system if the main thread exits prematurely. Another example, "zombie" processes take up process table entries.
Applicability
Consequences
Exceptions
Known Uses
Enforcement
Automation unlikely.

Avoid using reference counters

Rule
Do not use explicit reference counters to maintain reference information on a shared multi-threaded object.
Example
In JAWS, cached file objects may be accessed by more than one thread. Since the object cannot be destroyed until all the threads that are currently referencing it are done, the number of current references need to be maintained. The most straight forward solution is to apply an explicit reference count. A better solution is to use readers/writer locks to maintain an implicit reference count.
Rationale
Since the reference count of an object is modifiable state that is shared among all threads accessing the object, acquiring and releasing the object requires 2 synchronization calls each. The amount of synchronization overhead can be cut in half by using an implicit reference count maintained by a readers/writer lock. Acquiring the object corresponds to a acquiring a reader lock, and releasing the object corresponds to releasing the reader lock. By trying to obtain a writer lock, it can be determined whether or not the object is no longer being referenced.
Applicability
Caches, references to automatic variables (smart pointers), all applications related to reference counting.
Consequences
Reduces the synchronization overhead required to access shared objects. If the object is a database object that has been updated, additional complexity is needed to properly release the object from the database.
Exceptions
Only applies in multi-threaded (and concurrent programming) environments. For single-threaded applications, reference counters are easier to program and maintain.
Known Uses
JAWS Filecache.
Enforcement
Code review.

8. Real-time Rules

The rules in this section describe promote framework designs that lead to the development of more correct and efficient framework components.


Avoid unequal priority sharing

Rule
To minimize priority inversion, try not to share resources between threads of different priorities.
Example
Assume we are to implement a real-time system monitoring and control unit with 2 sensor inputs and a number of actuators. Suppose that one of the sensors supplies feedback at a higher rate than the other. Further assume the solution requires a separate thread to process the feedback from each sensor, and that a higher priority thread is used to process the input freom the high frequency sensor.

One possible design is to have both sensor inputs be modeled as prioritized events that are fed into a single event queue, and each thread queries the the event queue for their respective sensor input. The problem with this approach is that it may happen that the higher priority thread may be blocked out by the lower priority thread if the lower priority holds a lock on the queue while a new sensor input has come in for the higher priority thread. This results in priority inversion.

The better design is to dedicate a separte event queue for each thread, which removes contention for accessing the events.

Rationale
By not sharing resources between threads of different priorities, it is not possible for a lower priority thread to cause a higher priority thread to wait for the resource to be released.
Applicability
Consequences
Exceptions
Known Uses
Enforcement
Code review.

9. Internationalization Intents

As systems have to be ported to environments in which character sets are much larger than can be represented by 8 bits, internationalization of systems and frameworks can have a significant impact on design. These rules help reduce this impact.


Use an abstraction for char

Rule
Do not use the char type directly. Abstract it for wide characters.
Example
Suppose we are following the Create a wrapper for similar functions design rule. So, wrapper functions are created to utility functions that can operate on both regular char strings and wchar_t strings.

In ACE, this is accomplished by creating overloaded wrapper functions for these utility functions. For example, ACE provides the following typedef and wrappers for these two static functions.

#if defined (UNICODE)
typedef char TCHAR
#else
typedef wchar_t TCHAR
#endif

namespace ACE
{
static int strcmp (const char *s, const char *t);
static int strcmp (const wchar_t *s, const wchar_t *t);
}
Users of the framework can use TCHAR, and their code can then be easily ported to a UNICODE environment.
Rationale
This abstraction eases the task of porting applications to international environments.
Applicability
Consequences
When using the abstraction for the char type, it is no longer valid assume that a character only occupies a single byte.
Exceptions
Known Uses
Enforcement
Can be automated.

10. Conclusions


Last modified: Sun Mar 29 02:40:33 CST 1998 
 类似资料:

相关阅读

相关文章

相关问答