Huihoo.org - Open Enterprise Foundation

ACE中的线程安全接口模式

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

目的

线程安全接口模式保证组件内方法互调用时不会发生自死锁现象,同时能够最小化加锁带来的负载。

例子

在设计线程安全的组件时,设计者必须注意避免自死锁和不必要的加锁负载,特别是当组件方法发生互相调用时。为了能够说明这个情况,考虑一个更加具体的File_Cache的组件实例:

template
class File_Cache
{
public:
// Return a pointer to the memory-mapped file
// associated with , adding
// it to the cache if it doesn't exist.
const char *find (const char *pathname)
{
// Use the Scoped Locking idiom to
// automatically acquire and release the .
Guard guard (lock_);
const char *file_pointer = check_cache (pathname);
if (file_pointer == 0)
{
// Insert the into the cache.
// Note the intra-class method call.
bind (pathname);
file_pointer = check_cache (pathname);
}
return file_pointer;
}
// Add to the cache.
void bind (const char *pathname)
{
// Use the Scoped Locking idiom to
// automatically acquire and release the .
Guard guard (lock_);
// ... insert into the cache...
}
private:
// The strategized locking object.
LOCK lock_;
const char *check_cache (const char *);
// ... other private methods and data omitted...
};

这个File_Cache的实现仅仅在使用递归锁或是空锁的情况下才能够正常的工作。如果使用非递归锁,当find函数调用bind函数时,上面的代码将发生自死锁。因为bind函数需要已经被find函数锁定的LOCK。此外即使File_Cache使用递归锁,当锁在bind函数中再次被获取时将引发不必要的加锁负载。

语境

在多线程应用环境中的组件包含了内部方法的互相调用。

问题

多线程安全组件经常包含有多个接口和这些接口的实现。接口方法使用被封装在组件内部的状态信息以完成特定的计算功能。这些组件内部的状态信息被一个锁保护起来,通过串行化访问来防止竞争条件的发生。组件的方法在实现其功能的时候经常需要互相调用,对这种组件内方法相互调用的行为在多线程语义下不正确的设计将导致下面两个待解决的问题:
1、避免自死锁。线程安全组件应该被设计成不会发生自死锁现象。如果一个组件的方法在获取一个非递归锁后去调用另一个同样试图获取这个锁的组件方法将导致自死 锁。
2、最小化加锁负载。线程安全的组件应该被设计成在防止发生竞争条件的基础上尽量最小化加锁带来的负载。但是如果使用递归锁来避免自死锁的问题,将引发来自于多次组件内方法调用带来的加锁和解锁,从而增加额外的负载。

解决之道

构建存在组件内方法调用的组件应该基于下面两个设计惯例:
1、接口方法检测。所有的接口方法,一般为c++类中的公共方法,应该仅仅实现加锁和解锁的操作,因此实现同步检测的边界。当控制返回到调用者时,接口方法有责任完成解锁的操作。在获取一个锁后,接口方法应该调用一个实现方法,这个实现方法完成最终的具体方法功能。
2、实现方法信任。实现方法,一般为c++类中私有或保护方法,当被接口方法调用时仅仅实现具体的方法功能。这就是说,他们应该相信他们是在一个已经加锁的环境中被调用,不再需要任何的锁操作。这个环境已经时线程安全的。此外实现方法永远不要调用接口方法,因为那些方法需要加锁操作。

实现

线程安全的接口模式通过以下几个步骤实现:
1、 确定接口方法和相应的实现方法。接口方法定义了组件的公共接口,对于每个接口方法定义一个相应的实现方法。
template
class File_Cache
{
public:
// The following two interface methods just
// acquire/release the and forward to
// their corresponding implementation methods.
const char *find (const char *pathname);
void bind (const char *pathname);
private:
// The following two implementation methods
// do not acquire/release locks and perform the
// actual work associated with managing the .
const char *find_i (const char *pathname);
void bind_i (const char *pathname);
// ... Other implementation methods omitted ...

2、 定义接口方法和实现方法。接口方法和实现方法的定义要根据前面给出的安全线程接口的惯例。
下面是File_Cache类使用线程安全接口模式来防止自死锁和最小化加锁负载的实现:
template
class File_Cache
{
public:
// Return a pointer to the memory-mapped
// file associated with , adding
// it to the cache if it doesn't exist.
const char *find (const char *pathname)
{
// Use the Scoped Locking idiom to
// automatically acquire and release the .
Guard guard (lock_);
return find_i (pathname);
}
// Add to the file cache.
void bind (const char *pathname)
{
// Use the Scoped Locking idiom to
// automatically acquire and release the .
Guard guard (lock_);
bind_i (pathname);
}
private:
// The strategized locking object.
LOCK lock_;
// The following implementation methods do not
// acquire or release and perform their
// work without calling any interface methods.
const char *find_i (const char *pathname)
{
const char *file_pointer =
check_cache_i (pathname);
if (file_pointer == 0)
{
// If the isn't in the cache
// then insert it nto the cache and
// look it up again.
bind_i (pathname);
file_pointer = check_cache_i (pathname);
// The calls to implementation methods
// and , which
// assume that the lock is held and perform
// the work.
}
return file_pointer;
}
const char *check_cache_i (const char *)
{ /* ... */ }
void bind_i (const char *)
{ /* ... */ }
// ... other private methods and data omitted...
};

已知应用

线程安全接口模式被广泛的使用在ACE面向对象的网络编程工具中。

结论

在多线程安全组件的设计中使用线程安全接口模式将带来两点好处:
1、 增加组件的健壮性。确保在组件内方法互相调用时不会发生自死锁现象。
2、 提高性能。确保没有不必要的加锁和解锁过程。
使用线程安全的接口模式带来的弊端:增加了调用的非直接性和额外的方法定义。每一个接口函数都需要有一个对应的实现方法,这增加了组件的代码尺寸同时增加了调用的非直接性。一种可行的解决方法是使每个接口函数为inline。

译者论

1、这种模式将加锁的范围扩大,由原来的组件内部关键数据扩大到整个组件,感觉上降低了组件的并发能力。如果确保组件方法不会存在互相调用的可能,建议不要使用此模式。
2、该模式借鉴了JAVA语言中的同步机制,实现对象加锁和方法同步。
3、为实现c++的monitor对象提供的基础。