Abstract The use of the design patterns has been popularized with the publication of the Design Patterns book by Gamma et al. (1995). In this paper we discuss the implementation of the singleton design patterns from "error recovery'' perspectives for multi-threaded applications where dynamic memory is allocated on a task by task basis. The description of the singleton pattern is straight forward but its implementation issues are complicated [Alexandrescu, 2001]. We highlight a few problems of traditional implementation for multi-threaded applications and propose several alternatives that could potentially be used in different contexts depending on the problem at hand. Because of the popularity of the C ++ programming languages in the industry, we also present sample implementation details in C ++ . 1 THE SINGLETON PATTERN In real world applications there are many situations where there can be one and only one instance of a class. Typical examples may include a class for logging events to a serial output device, a class for authenticating and verifying passwords entered in a website, etc. The purpose of the singleton pattern, in the context of object-oriented programming, is to provide a generic mechanism such that the class is instantiated only once. Normally the first invocation of the class creates the unique instance of the class and subsequent invocations would simply return a reference (1) to the instance created earlier. Figure 1 represents the Object Model Diagram (OMD) (after Gamma et al., 1995) of singletons implemented in the traditional (2) way (e.g. Figure 2).
As shown in the code fragment (Figure 2), the mechanism for implementing the singleton pattern is quite straight forward in C ++ . A class specific static variable instance is defined to control creation of more than one instances and the static member function getInstance is used for accessing the one and only one instance of the singleton class (3). Initially the static class variable Singl::instance is initialized to 0 (by default) and when the static function Singl::getInstance is invoked (in the main routine), an instance of the Singl class is newed in the heap and the instance pointer is returned to the application. Normally constructors and copy constructors of the singleton class are made private or protected such that an attempt to instantiate or copy the Singl (class on its own) is flagged as an error during compilation. 2 SINGLETON PROBLEMS The code given in Figure 2 works well for a single threaded application. If there are a number of tasks or threads accessing the singleton object/class (O/C) in an application, the first task executing the singleton O/C allocates memory for the instance variable. Since subsequent singleton access within this task and other tasks (4) are read only, the above mechanism also works for a well-behaved multi-tasking application (5). However the code given in Figure 2 can create problems if the singleton pattern is in a layer which is common to a number of tasks as shown in Figure 3.
In Figure 3, there are three tasks which use the services of the underlying lower (or common) layer. Let us assume that the common layer has a single O/C implementing the singleton pattern as shown in Figure 2. The layering approach illustrated in Figure 3 is quite common in many applications (including real-time/embedded systems) whereby the upper layers use the services provided by the lower layers. In an object-oriented application, the lower layers could be a generic framework or a set of reusable class libraries. Let us further assume that the tasks Task1, Task2, and Task3 all use the singleton instance implemented in the common layer. Let us now examine a few situations while the application is being executed:
So far so good. All the tasks are behaving properly and all tasks can access the global static variable pointer instance. Now because of some problems, let us assume that Task1 starts to misbehave and is terminated (6). When a task is terminated, the heap memory associated with that task would no longer be valid and is likely to contain an invalid address (7). This would cause the state of the singleton class to be altered. Since the memory for the singleton instance comes from Task1's address space, an attempt to access the singleton class either by Task2 or Task3 is likely to cause a core dump or a bus error. This may bring down the whole application. There may be a number of situations where it may not cause any catastrophic effects when the problems in one task propagate to the rest of the application. However this behavior would not be normally tolerated in a mission-critical, multi-tasking applications such as a wireless switch. In such systems it is desirable to confine all problems to the task itself as far as possible such that the errant task can be restarted again to resume the operation.
3 POSSIBLE ENHANCEMENTS Figure 5 shows one possible strategy to fix the problem [Alexandrescu, 2001] of the singleton pattern as discussed in the last section. The static pointer variable instance of Figure 2 has been removed and is replaced by an instance singl of the class Singl. Thus an instance of the Singl class would automatically be created when the code object is executed/downloaded in the default data segment. The getInstance method returns the address of the singl object. Since static variables are initialized only once, subsequent invocations of the getInstance method would not create another instance of the Singl class.
The above approach may work in an operating system environment which has flat memory address space and global data segment can be accessed by all of the tasks/threads. But this may create some problems in an operating system architecture where by global data is not allowed and individual tasks have corresponding heap and data segment exclusively for themselves. Furthermore, this approach may also have performance related issues as illustrated in Figure 6. There are three tasks and three possible message queues in Figure 6. If there is a single class, all of the messages posted to all of the queues is routed via. this class and a filtering mechanism needs to be implemented in the singleton class to route the messages to different tasks appropriately. If the singleton class also implements a publish-subscribe design pattern and if the message filtering is done per message type, this list could potentially be huge and there may be issues while the list is traversed. Another alternative fix would be to create a task specific array of memory (at the stack) and associate this memory with singleton instances as shown in Figure 7 where the code that implements the singleton pattern resides in the common layer, whereas the memory for the singleton instance is allocated in each task's address space. There are three pointers (one per each task) pointing to the singleton class and share the same logic for the access operation from the common layer. If the task e.g. Task1 dies, then only the instance pointer associated with this task would point to an invalid address, without affecting the other two tasks and the rest of the application. Thus in summary the problems caused by any one task would be localized and would not be propagated to the rest of the tasks/applications.
Figure 6. Performance problem of the global instance
One of the possible implementation strategy for the singleton architecture of Figure 7 is shown in Figure 8. As shown in Figure 8, the enum SinglEnum lists all of the singletons across the application (8). Both the declaration and the definition of the static variable instance have been removed from the Singl class. The main entry point of each task defines an array big enough to hold pointers to the instances of the singletons (for that task). Thus the singletons now use stack memory from each tasks address space. This memory address is then associated with the current task such that this memory is used (after context switch) when the singleton code is executed (9). In summary, by using this approach, we are creating instance pointers equal to the number of unique tasks accessing the singletons and associating them with the tasks appropriately.
Another alternative implementation strategy would be to create a Manager class that would be responsible for managing the singletons throughout the application. The object model diagram for such an arrangement is shown in Figure 9. The class ISingl is the interface class and all singletons throughout the application inherit from this class. The class SinglMgr manages instances of the ISingl class pointers. SinglMgr is a friend to Singl class such that it can access appropriate constructors and destructors.
The code fragment shown in Figure 10 implements the OMD of Figure 9. As in Figure 8, the code fragment defines an enum of singletons used in the application. It then defines an interface class (ISingl) with a virtual destructor. The advantage of adding the interface class ISingl is in the destruction of the singletons (discussed later). The singleton Manager SinglMgr maintains a list of the singletons (pointer to ISingl class). The memory for this list is defined at the main entry point on the stack. The SinglMgr class has a getInstance method that takes the singleton index from the enum and returns a ISingl pointer which can be casted to an appropriate type (derived class). As mentioned earlier all of the singletons (e.g. Singl) publicly inherit from the ISingl class and define SinglMgr as a friend to ISingl. The destructor is virtual (to ensure correct destruction) but protected (defined in the ISingl class) such that the memory is freed appropriately when ISingl pointers are used (within the SinglMgr object) to delete child singleton objects (10) and to allow other classes to derive from ISingl. As in the earlier examples, the constructors are in the private section. Furthermore, this class does not have any methods to get the singleton instance. The responsibility of getting an instance of all the singletons has been delegated to the SinglMgr class. The constructor of the SinglMgr class does a new to instantiate individual singletons. Thus the construction order (as well as destruction) of singletons can be explicitly specified if necessary. The destructor iterates through the array to delete singleton instances. Alternatively the destructor can be modified to specify the dependency order in which singletons need to be destroyed (e.g. one singleton depends on the other singleton) without impacting the rest of the application. The main routine defines an array of ISingl pointers (in its stack) sufficient to hold a pointer to all singletons. It then instantiates SinglMgr class (11)and passes the address of the array as the constructor parameter. The next line shows how to get an instance instance of a singleton. The constructor of the SinglMgr class initializes the singleton pointers by creating a new instance explicitly. Thus as long as the SinglMgr constructor is invoked successfully, it can be guaranteed the existence of the singleton instances. Hence the SinglMgr::getInstance method does not check whether the singleton instance is pointing to a NULL (0) object. As a result this implementation approach may be slightly faster than the earlier proposal, specially if singletons are accessed quite frequently in an application. As an added advantage, the singletons would be automatically deleted when the SinglMgr object goes out of scope. The code fragment presented in Figure 10 may be suitable for some application areas where the number of singletons are small and known before hand and both the SinglMgr and ISingl are in the same package. If they are in different packages (e.g. a ISingl and SinglMgr in a library and Singl in application code), this implementation strategy introduces circular dependencies as SinglMgr needs to have a knowledge of Singl. As the SinglMgr is responsible for managing all of the singletons in an application, adding a new singleton requires modification of this class. There is also a loss of type information of ``application singletons'' in the getInstance method as the SinglMgr stores an array of ISingl object pointers.
One method of overcoming several problems of Figure 10 is illustrated in Figure 11 (OMD) and the corresponding C ++ code is in Figure 12. In this method the library or the common layer defines the classes base_singleton, id, singleton and singleton_manager. The class singleton is a template class (T being the template parameter and inherits from base_singleton. There is a static member id_ inside the singleton class for each type of application specific singletons (e.g. my_singleton). In other words, given two application specific singleton classes singl1 and singl2, both singleton<singl1>::id_> and singleton<singl2>::id_ exist and are two distinct objects with distinct values. The class singleton_manager holds an array of singletons. It has a member function template <class T> static T& instance( T* = 0 ); The compiler generates the requested overloaded instance functions when needed (12). The class base_singleton contains an id class (one for each application specific singletons (e.g. my_singleton) to determine whether the singleton has already been instantiated.
With this approach as the list of singleton objects (and their corresponding pointers/aliases) are automatically maintained in a vector list, the code is easier to maintain. This also removes circular dependency as the singletons are created and added to the list automatically/dynamically within the application domain. Furthermore, this solution removes the possibility of having a mismatch between static_cast and the list of enumerations (by application developers). The implementation uses Curiously Recurring Template Patterns [Coplien, 1995], which gives the base class a way of knowing a derived class at compile time (type information is not lost) without creating circular compile time dependencies. However on the down side, this implementation strategy has memory overhead from the class id. The use of dynamic_cast trades some run-time performance overhead for safety. Figure 13 shows yet another alternative implementation. In contrast to the implementation of Figure 12, this implementation uses std:vector for keeping all the singletons within an application. The second solution also does not loose any type information, when the pointers to singleton objects are stored. Thus it does not require any casting when the singleton pointer is accessed. This also does not have any memory overhead from the id class (one per templated class). However this does not address any issues about multi-threaded applications. 4 SUMMARY The singleton pattern deals with a generic way of controlling instances of a class in an application. The traditional use of the singleton pattern can cause problems in mission-critical multi-tasking environment when the singleton pattern is present in a layer that is common to all of the tasks and each task has its exclusive dynamic memory pool. In this paper we have discussed one problematic area in the context of multi-tasking programming environment and have proposed solutions that may be appropriate in some situations. We have also presented different variations of the implementation strategy and presented sample implementation details in C ++. Table 1 summarizes the pros and cons of different implementation strategy. There may be different factors that govern the choice of one implementation strategy over the other. Table 1. Summary of different implementation strategy
5 ACKNOWLEDGEMENTS The authors wish to thank Mogens Hansen for providing constructive criticisms on earlier drafts of this paper, answering many implementation related questions promptly and suggesting two alternative ways of implementing the singleton pattern that do not have any circular dependencies. Amit Bhonsle helped with the implementation of some of the ideas presented in this paper. Footnotes 1 The term reference in this context is used generically unless stated otherwise. But this may be implemented as a pointer in C ++ . 2 We distinguish between traditional implementation with the extensions presented in this paper. 3 The main objective of the paper is to analyze some of the error scenarios in multi-threaded applications. Thus to illustrate the concepts only, the implementation has been kept very simple and intentionally ignores some of the consequences of the Singleton Pattern [Gamma et al., 1995] such as refinements of operations and representation. 4 Here we are assuming that the memory allocated in the heap by one task is accessible to other tasks as well and the dynamic memory is allocated per task basis. In other words each task has its exclusive pool of dynamic memory. Furthermore it is assumed that the singletons are defined in a common lower layer (such as a library). 5 We have not examined the effect of multiple processors and parallel processing in this paper. 6 Errors in a task means memory problems (within that task), division by zero, or other internal failures. Normally when a task within an application encounters a problem, the entire application will be in an undefined state. However our application does not use any shared memory and there is no other coupling between any two tasks. All tasks have their own heap, their own queues for receiving messages and they communicate with each other by sending messages. Thus our aim is to create tasks that are potentially isolated from each other. 7 As discussed earlier, in our software architecture, each task has its associated memory heap which is preallocated when the task is started. When the task is killed and restarted, the singleton pointers for that task would be initialized at the beginning (discussed below). REFERENCES [Alexandrescu, 2001] Alexandrescu, A., Modern C ++Design: Generic Programming and Design Patterns Applied, Addison-Wesley Longman, Inc., ISBN: 0201704315, 352 pages, 2001. Yagna Pant is a Senior Staff Engineer in the CDMA Software Development Group in Global Telecom Solutions Sector at Motorola. He has a Masters degree in Software Engineering from the University New South Wales, Sydney, Australia. He works on the development of object-oriented, real-time software. His other interests are object-oriented design patterns, software reuse, software metrics, cellular systems, network management and voice over Internet Protocol. He can be contacted at Yagna.Pant@motorola.com. Cite this article as follows: Yagna Pant, Kazuhiro Ondo: "Thread Specific Singletons: Handling Singleton Pattern Errors in Multi-Threaded Applications and their Variations", in Journal of Object Technology, vol. 1, no. 2, July-August 2002, pp. 155-169, http://www.jot.fm/issues/issue_2002_07/article4 |