|
COM Singletons - A Dangerous Animal Author: Richard Blewett What is a singleton? A singleton is a design pattern that allows all clients to create an apparent shared, single instance of an object. Consider holding session information within an application: all objects need to be able to access the same session information. To avoid having to use some documented path to the information to which all components must adhere, it is much safer and more flexible to be able to create a session object which, to all intents and purposes, is the session information. One way of implementing this in COM is to use a COM singleton - a single COM identity that all calls to CoCreateInstance return. This involves the class factory always returning the same instance from all calls to IClassFactory::CreateInstance. ATL does this by creating the single identity when it creates the class factory and handing out a reference when CreateInstance is called. You achieve this behaviour by placing the DECLARE_CLASSFACTORY_SINGLETON in your class definition, i.e.
Class ATL_NO_VTABLE CSingletonFoo :
Public CComObjectRootEx<CComSingleThreadModel>,
Public CComCoClass<CSingletonFoo, &CLSID_SingletonFoo>,
Public IDispatchImpl<ISingletonFoo, &IID_ISingletonFoo, &LIBID_SINGLETONSLib>
{
public:
CsingletonFoo()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_SINGLETONFOO)
However, COM Singletons are, at best, a flawed design approach and, at worst, potentially lethal to your application. In-Proc Singletons - Highly Dangerous, Do Not Approach So, why are in-proc singletons dangerous? To explain this we have to look at their usage, let's consider an STA based non-singleton object, Foo: Figure 1 shows a sequence of events:
Now lets make Foo a COM Singleton: Figure 2 shows the sequence of events:
So why is this so bad? Foo is an STA based component. It is, therefore, unlikely to have been written thread-safe. Now, one thread runs in STA1 and another in STA2 - but both apartments have direct access to Foo which means that there is nothing to stop both apartments accessing Foo concurrently - for a non-threadsafe object, this is not good news. So, OK we can make Foo threadsafe, is this enough to make sure everything works as we expect - sadly, no. Consider Foo having a member variable that is an interface pointer to another object, say Bar. We have two possibilities:
So, interface pointers as member variables will go horribly wrong unless we do something about it. Luckily, the Global Interface Table (GIT) comes to the rescue. The Global Interface Table The Global Interface Table (GIT) was introduced in NT4 service pack 3 and DCOM95 1.1. The GIT is a system provided COM Object for marshalling interface pointers, between apartments, within a process. Prior to the GIT, there was no way to marshal any interface pointer such that it could be unmarhsalled many times. CoMarshalInterThreadinterfaceInStream would give you a single opportunity to unmarshal, but for a member variable we need to be able to hold an interface pointer such that we can unmarshal it many times- every time it is requested, say via a property. The GIT allows us to do this and is created as follows:
IGlobalInterfaceTable* pGIT = NULL;
HRESULT hr = CoCreateInstance( CLSID_StdGlobalInterafceTable,
NULL,
CLSCTX_INPROC_SERVER,
IID_IglobalInterfaceTable,
reinterpret_cast<void**>(&pGit) );
IGlobalInterfaceTable has 3 methods: RegisterInterfaceInGlobal, GetInterfaceFromGlobal and RevokeInterfaceFromGlobal and the IDL for it looks like this:
Interface IGlobalInterfaceTable : Iunknown
{
HRESULT RegisterInterfaceInGlobal( [in] IUnknown *pUnk, [in] REFIID riid,
[out] DWORD *pdwCookie );
HRESULT RevokeInterfaceFromGlobal( [in] DWORD dwCookie );
HRESULT GetInterfaceFromGlobal( [in] DWORD dwCookie, [in] REFIID riid,
[out, iid_is(riid)] void **ppv );
};
An interface is registered in the GIT by calling RegisterInterfaceInGlobal, this returns a DWORD cookie which can be stored globally as it is not bound to a particular apartment (unlike an interface pointer). When an apartment needs the interface pointer, it calls GetInterfaceFromGlobal which returns an interface pointer which is marshalled correctly for the callers apartment. When there is no more need to hold an interface globally, RevokeInterfaceFromGlobal must be called to remove the interface pointer from the GIT. Making an object threadsafe, accessing interface pointers via the GIT, some of you may have noticed that STA based inproc singletons seem to have to be coded the same way as components that aggregate the FTM - and you'd be absolutely right. But aggregating the FTM is explicitly providing this behaviour, creating a singleton is providing this by accident. So if inproc STA based singletons are unpleasant, are there any COM singletons that behave OK? Well, inproc objects which are Free or Single threaded behave OK as they are bound to a single apartment (MTA and main STA repectively); also, out of proc singletons behave OK as well. So, is it generally OK to use singletons? COM Singletons, a Flaw in the Design Lets look at the concept of a singleton. A singleton is meant to provide a single view of an object to the world. Implementing a singleton as a single COM identity forces a number of design decisions:
So, given that COM singletons cause problems both in terms of design, and implementation, is there any way singleton like behaviour can be exposed without being bound to a single COM identity? To answer this we have to look at what a singleton gives us - the ability for clients to access shared behaviour and state. The Shared State / Multiple Identity Model Imagine an object that dispenses resources, which are of a finite number. These resources are acquired and handed back by the clients (resources could be threads in a thread pool, serial ports, etc.). When a client is finished with the resource it releases it back to the pool. A singleton could be used to manage the finite pool, keeping the number of acquired resources in a member variable of the object.
class ResourceManager : public ImanageResources
{
DWORD m_dwNumAvailableResources;
HRESULT AcquireResource()
{
if(m_dwNumAvailableResources == 0)
return E_FAIL;
m_dwNumAvailableResources--;
return S_OK;
}
HRESULT ReleaseResource()
{
m_dwNumAvailableResources++;
return S_OK;
}
};
However, there is another way to implement this:
class ResourceManager : public ImanageResources
{
static DWORD s_dwNumAvailableResources;
DWORD& m_dwNumAvailableResources;
ResourceManager()
: m_dwNumAvailableResources(s_dwNumAvailableResources)
{
}
HRESULT AcquireResource()
{
if(m_dwNumAvailableResources == 0)
return E_FAIL;
m_dwNumAvailableResources--;
return S_OK;}
HRESULT ReleaseResource()
{
m_dwNumAvailableResources++;
return S_OK;
}
};
Using a static variable to hold the number of available resources allows multiple instances of the ResourceManager object to access a common state. This gives a much more flexible model where the location of the common state can be altered with no knowledge of the client that the scope of the 'singleton' behaviour has been extended. For example, with an in-proc server: we can hold the shared state in static variables and have per process singleton scope; we can hold state in shared memory and have machine wide singleton scope; we can hold the state in a database and have network wide singleton scope - all with no semantic change as regards the client. Also, these objects have state that doesn't have to be shared by all clients, for example the reference count of the object. With a singleton, as previously discussed, it is often the case that access to all state must be synchronised. With the shared state multiple identity model we only need protect the shared state. In other words, singletons must be thread-safe at the object level, whereas shared state multiple identity model objects need only be thread-safe at the class level. This gives two advantages of the shared state multiple identity model over the singleton: there is less thread synchronisation code to write; there is less contention for resources (and so theoretically more throughput). Now suppose that we've sold a lot of these resource dispensers, but it's become apparent that the licensing agreement is being abused. We decide, not to stop people using the product when they exceed their license count, but to send an automatic email to ourselves so we can charge more. With a singleton we have problems: the only time we know that a new client wants an object is via the class factory, so we have can increment a count in CreateInstance to reflect the number of clients. However, how do we know that a client has released the resource? Unfortunately there is only one stub for this object so we don't see every Release(). There is no sure-fire way of finding out the usage of the component without the client's cooperation (say through a Register, Unregister function pair). With a shared state multiple identity model component we can simply increment and decrement the usage count in the constructor / destructor (or FinalConstruct / FinalRelease), or in fact IObjectControl::Activate / IObjectControl::Deactivate. This is achieved with no change to the client code and so we can implement our new licensing model without the clients explicit cooperation (implicitly, they will have to install the new code). It is impossible to pre-empt completely the way an object may need to evolve. With the single state multiple identity model a far higher degree of flexibility is available. The Bottom Line OK, I've argued that COM singletons are bad but their functionality can be implemented in a much more flexible way using the Shared State multiple identity model. Does that mean you should never use a singleton? If the problems with singletons that I've outlined are of no concern to you, by all means use a singleton, just don't say you haven't been warned. I often use singletons in testing or samples, as a way to allow multiple clients to hit the same state, as they are very quick to implement in ATL. However, I would not now implement a singleton in production code (I have in the past - I too was seduced by the DECLARE_CLASSFACTORY_SINGLETON macro in ATL). There is also a half-way house I which you internally use a singleton but wrap it in another 'normal' object so clients never 'see' the singleton which manages the shared part of the state. However, this does seem like using a singleton out of pure obstinacy. Sharing state amongst multiple COM identities provides a much more flexible model than singletons and achieves the same result. There is normally at bit more code to write, but I would argue that given the problems and restrictions that it brings DECLARE_CLASSFACTORY_SINGLETON is one of the most dangerous lines of code in ATL. Richard Blewett is a freelance consultant and part of the DevelopMentor COM+ team. He has been involved in systems development for 10 years and got his first exposure to COM in 1995. He is currently working for Securicor Information Systems as a middle tier architect, designing and writing business objects in ATL. You can contact him at [email protected]. Contribute to IDR: To contribute an article to IDR, a click here.
|
|