Huihoo.org - Open Enterprise Foundation

ACE策略化的加锁模式

(作者:Douglas C. Schmidt ,by huihoo.org CORBA课题 Thzhang 译)

目的

策略化的加锁模式通过策略化一个组件的同步机制,在不降低其性能或可维护性的基础上增加组件的灵活性和可重用性。

例子

用于实现高性能WEB服务器的关键组件是文件缓冲,其将一个URL路径名映射到一个内存映象文件。当用户请求一个已经被缓存的URL路径时,WEB服务器可以直接将内存映象文件内容传回给客户端,而不需要经过多次读、写操作来访问缓慢的二级存储设备。一个用于高可移植性的高性能WEB服务器的文件缓冲的实现,必须能够在各种多线程或单线程的操作系统下能够高效率的运行。一种获取此种移植性的方法是开发下面展示的文件缓冲类:


// A multi-threaded file cache implementation.
class File_Cache_Thread_Mutex {
public:
// Return a pointer to the memory-mapped
// file associated with .
const char *find (const char *pathname)
{
// Use the Scoped Locking idiom to serialize
// access to the file cache.
Guard guard (lock_);
// ... look up the file in the cache, mapping it
// into memory if it is not currently in the cache.
return file_pointer;
}
// ...
private:
//File cache implementation...
// Synchronization strategy.
Thread_Mutex lock_;
};
// A single-threaded file cache implementation.
class File_Cache_ST
{
public:
// Return a pointer to the memory-mapped
// file associated with .
const char *find (const char *pathname)
{
// No locking required since we are
// single-threaded.
// ... look up the file in the cache, mapping it
// into memory if it is not currently in the cache.
return file_pointer;
}
// ...
private:
//File cache implementation...
// No lock required since we are
// single-threaded.
};


这两种实现形式共同组成一个组件家族的一部分,它们的不同仅仅在于同步的策略。一个组件家族成员File_Cache_ST实现单线程的文件缓冲,不需要加锁机制。另一个组件家族成员File_Cache_Thread_Mutex的实现使用互斥量来串行化并发线程访问。但是维护多个分离的、功能相近的文件缓冲组件是冗余的,因为将来的功能增加、错误修正都要针对每一个组件的实现进行。

语境

一个应用或系统中的可重用组件必须能够在各种并发的用例下被使用。

问题

一个同步策略是通过硬编码实现的可重用组件族,将不能解决如下问题:
1、轻松的性能调节。应该可以通过简单的调节使一个组件使其适应特定的并发用例。但是,如果同步机制是通过硬编码实现,改动一个组件使其支持新的更有效率的同步策略将是耗时的过程。 例如,为了在一个大尺度、多处理器的平台上增加性能,必须使用读写锁替代线程互斥量来完全重写一个新的File_Cache_RW_Lock类实现。

2、轻松的维护和改进。新的改进或错误修正应该是简单的。但是如果有一个基础组件的多个拷贝,版本的爆炸的情况将很可能会发生,因为对一个组件的修改不会自动影响到其他组件的实现。 例如,对单线程文件缓冲的算法进行改进,必然清晰的影响到文件缓冲组件族中所有相关的类,这将是繁琐的和易出错的。

解决之道

通过标记它们为"可入的"类型来策略化一个组件的同步方面。定义这些可插入类型的实例为组件的数据成员,它们被组件的方法用于实现被应用或服务制定在组件内部的同步策略。

实现

策略化的加锁模式可以通过下面的几个步骤实现:
1、定义组件的接口和实现。这个步骤的焦点在于定义简明的组件接口和一个不考虑同步问题的有效率的实现。 下面的类定义了file_cache的接口和实现:

class File_Cache
{
public:
const char *find (const char *pathname);
// ...
private:
// File_Cache data members and methods go here...


2、 策略化易变的同步方面(aspect)。在这个步骤中,确定组件中的哪些是可能引起组件的接口和实现发生变化和修改的同步方面,将其策略化。 许多可重用组件有相对简单的同步方面,它们可以通过象互斥量和信号量等普通的加锁策略来实现。这些同步方面可以使用参数化类型或多态方法来策略化成一种一致的行为。
多态方法:在这个方法中,传递一个多态锁LOCK象到组件的初始化方法,同时定义这个LOCK对象实例作为组件的私有数据成员用于实现组件方法的加锁策略。 一种实现多态LOCK对象的通用方法是使用桥模式。首选我们定义一个抽象的锁类,包含有多态的acquire 和release方法,如下:


class Lockable
{
public:
// Acquire the lock.
virtual int acquire (void) = 0;
// Release the lock.
virtual int release (void) = 0;
// ...
};


子类必须重载Lockable中的纯虚方法来定义具体的锁策略。例如,下面的类定义了Thread_Mutex锁:

class Thread_Mutex_Lockable : public Lockable
{
public:
// Acquire the lock.
virtual int acquire (void) {
return lock_.acquire ();
}
// Release the lock.
virtual int release (void) {
return lock_.release ();
}
private:
// Concrete lock type.
Thread_Mutex lock_;
};


最后我们应用桥模式定义一个非多态的接口类,在其中包含一个多态Lockable对象的引用:

class Lock
{
public:
// Constructor stores a reference to the base class.
Lock (Lockable &l): lock_ (l) {};
// Acquire the lock by forwarding to the
// polymorphic acquire() method.
int acquire (void) { lock_.acquire (); }
// Release the lock by forwarding to the
// polymorphic release() method.
int release (void) { lock_.release (); }
private:
// Maintain a reference to the polymorphic lock.
Lockable &lock_;
};



这个类的目的是保证lock被用作一个对象,而不是指向基类的指针。这种设计使它可以重用利用多态锁策略实现的区域锁习惯用法:

class File_Cache
{
public:
// Constructor
File_Cache (Lock lock): lock_ (lock) {}
// A method.
const char *find (const char *pathname)
{
// Use the Scoped Locking idiom to
// acquire and release the  automatically.
Guard guard (lock_);
// Implement the find() method.
}
// ...
private:
// The polymorphic strategized locking object.
Lock lock_;
// Other File_Cache data members and methods go here...


参数化类型:在这个方法中,给组件增加一个适当的LOCK模板参数,同时定义一个LOCK实例作为组件的私有数据成员用于实现组件方法的锁策略。
下面展示了通过LOCK模板参数来策略化的File_Cache组件:

template  class File_Cache
{
public:
// A method.
const char *find (const char *pathname)
{
// Use the Scoped Locking idiom to
// acquire and release the  automatically.
Guard guard (lock_);
// Implement the find() method.
}
// ...
private:
// The parameterized type strategized locking object.
LOCK lock_;
// Other File_Cache data members and methods go here...


使用这个实现,模板可以被任意类型的LOCK来实例化,被区域锁习惯用法用于实现acquire 和release操作。需要特别指出的是,使用实例化模板的模板参数LOCK不需要从一个抽象的基类继承,如lockable。

一般情况下,当锁策略在编译时刻就可以确定,应该使用参数化类型方法。当锁策略直到运行时才可以确定,应该使用多态方法。通常,在模板运行时性能和使用多态带来的潜在的运行时扩展性之间综合考虑。
3、定义锁策略族。这个家族中的每个成员都必须提供统一的接口,可以支持各种应用指定的并发用例。如果同步组件不存在或是存在,但有不兼容的接口,使用包装门面模式(Wrapper Facade pattern)或适配方式使其适应组件同步方面希望的样式。 除了使用包装门面模式定义的Thread_Mutex外,还包括读写锁、信号量、互斥量和文件锁等其他普通锁策略。一个令人惊奇的锁策略是NULL_MUTEX。这个类为单线程的应用和组件定义了一个高效的锁策略,如下:

class Null_Mutex
{
public:
Null_Mutex (void) { }
Null_Mutex (void) { }
int acquire (void) { return 0; }
int release (void) { return 0; }
};


所有NULL_MUTEX的方法都是空的内联函数,可以完全被编译器优化掉。

结论

在可重用组件中应用策略化锁模式可带来两点益处:
1、增加灵活性和性能的调节。因为组件的同步方面被策略化了,这将直接配置、调节一个组件适应特殊的并发用例。
2、减少对组件的维护投入。它直接增强组件的功能,防止出错,因为这里只有一个实现而不是对每一个并发用例都有一个分离实现。这种集中化的考虑避免了版本的爆炸。
在可重用组件中应用策略锁模式可导致如下缺点:
唐突(Obtrusive)的加锁方式。如果使用模板来参数化加锁方式,将使加锁策略暴露给整个应用代码。这种设计是唐突的,特别是当编译器不能有效的支持模板时。避免这个问题的一种方法是多态技术来策略化组件的加锁行为。