ACE技术论文集-第2章 包装外观 Wrapper Facade
发布: 2008-6-13 15:00 | 作者: Prashant Jain and D | 来源: 转载 | 查看: 423次
Thread_Manager还提供联接(join)和取消线程的方法。
1. 确定错误处理机制:低级的C函数API通常使用返回值和整型代码(比如errno)来将错误通知给它们的调用者。但是,此技术是容易出错的,因为调用者可能会忘记检查它们的函数调用的返回状态。
更为优雅的报告错误的方式是使用异常处理。许多编程语言,比如C++和Java,使用异常处理来作为错误报告机制。它也被某些操作系统所使用,比如Win32。
使用异常处理作为包装外观类的错误处理机制有若干好处:
- 它是可扩展的:现代编程语言允许通过对现有接口和使用干扰极少的特性来扩展异常处理策略和机制。例如,C++和Java使用继承来定义异常类的层次。
- 它使错误处理与正常处理得以干净地去耦合:例如,错误处理信息不会显式地传递给操作。而且,应用不会因为没有检查函数返回值而偶然地忽略异常。
- 它可以是类型安全的:在像C++和Java这样的语言中,异常以一种强类型化的方式被扔出和捕捉,以增强错误处理代码的组织和正确性。相对于显式地检查线程专有的错误值,编译器会确保对于每种类型的异常,将执行正确的处理器。
但是,为包装外观类使用异常处理也有若干缺点:
- 它不是通用的:不是所有语言都提供异常处理。例如,某些C++编译器没有实现异常。同样地,当OS提供异常服务时,它们必须被语言扩展所支持,从而降低了代码的可移植性。
- 它使多种语言的使用变得复杂化:因为语言以不同的方式实现异常,或根本不实现异常,如果以不同语言编写的组件扔出异常,可能很难把它们集成在一起。相反,使用整型值或结构来报告错误信息提供了更为通用的解决方案。
- 它使资源管理变得复杂化:如果在C++或Java代码块中有多个退出路径,资源管理可能会变得复杂化[10]。因而,如果语言或编程环境不支持垃圾收集,必须注意确保在有异常扔出时删除动态分配的对象。
- 它有着潜在的时间和/或空间低效的可能性:即使没有异常扔出,异常处理的糟糕实现也会带来时间和/或空间的过度开销[10]。对于必须具有高效和低内存占用特性的嵌入式系统来说,这样的开销可能会特别地成问题。
对于封装内核级设备驱动程序或低级的本地OS API(它们必须被移植到许多平台上)的包装外观来说,异常处理的缺点也是特别成问题的。对于这些类型的包装外观,更为可移植、高效和线程安全的处理错误的方式是定义错误处理器抽象,显式地维护关于操作的成功或失败的信息。使用线程专有存储(Thread-Specific Storage)模式[11]是被广泛用于这些系统级包装外观的解决方案。
1. 定义相关助手类(可选):一旦低级函数和数据结构被封装在内聚的包装外观类中,常常有可能创建其他助手类来进一步简化应用开发。通常要在包装外观模式已被应用于将低级函数和与其关联的数据聚合进类中之后,这些助手类的效用才变得明显起来。
例如,在我们的日志例子中,我们可以有效地利用下面的实现C++ Scoped Locking习语的Guard类;该习语确保Thread_Mutex被适当地释放,不管程序的控制流是怎样退出作用域的。
template
class Guard
{
public:
Guard (LOCK &lock): lock_ (lock)
{
lock_.acquire ();
}
?Guard (void)
{
lock_.release ();
}
private:
// Hold the lock by reference to avoid
// the use of the copy constructor...
LOCK &lock_;
}
Guard类应用了[12]中描述的C++习语,藉此,在一定作用域中“构造器获取资源而析构器释放它们”。如下所示:
// ...
{
// Constructor of automatically
// acquires the lock.
Guard mon (mutex);
// ... operations that must be serialized ...
// Destructor of automatically
// releases the lock.
}
// ...
因为我们使用了像Thread_Mutex包装外观这样的类,我们可以很容易地替换不同类型的锁定机制,与此同时仍然复用Guard的自动锁定/解锁协议。例如,我们可以用Process_Mutex类来取代Thread_Mutex类,如下所示:
// Acquire a process-wide mutex.
Guard mon (mutex);
如果使用C函数和数据结构、而不是C++类,获得这种程度的“可插性”(pluggability)要困难得多。
下面的代码演示日志服务器的main函数,它已使用2.2.8描述的互斥体、socket和线程的包装外观重写。
// At file scope.
// Keep track of number of logging requests.
static int request_count;
// Manage threads in this process.
static Thread_Manager thr_mgr;
// Lock to protect request_count.
static Thread_Mutex lock;
// Forward declaration.
static void *logging_handler (void *);
// Port number to listen on for requests.
static const int logging_port = 10000;
// Main driver function for the multi-threaded
// logging server. Some error handling has been
// omitted to save space in the example.
int main (int argc, char *argv[])
{
// Internet address of server.
INET_Addr addr (port);
// Passive-mode acceptor object.
SOCK_Acceptor server (addr);
SOCK_Stream new_stream;
// Wait for a connection from a client.
for (;;)
{
// Accept a connection from a client.
server.accept (new_stream);
// Get the underlying handle.
SOCKET h = new_stream.get_handle ();
// Spawn off a thread-per-connection.
thr_mgr.spawn (logging_handler,
reinterpret_cast (h),
THR_DETACHED);
}
}
logging_handler函数运行在单独的线程控制中,也就是,每个相连客户有一个线程。它在各个连接上接收并处理日志记录,如下所示:
// Entry point that processes logging records for
// one client connection.
void *logging_handler (void *arg)
{
SOCKET h = reinterpret_cast (arg);
// Create a object from SOCKET .
SOCK_Stream stream (h);
for (;;)
{
UINT_32 len; // Ensure a 32-bit quantity.
char log_record[LOG_RECORD_MAX];
// The first reads the length
// (stored as a 32-bit integer) of
// adjacent logging record. This code
// handles "short-s".
ssize_t n = stream.recv_n
(reinterpret_cast (&len),
sizeof len);
// Bail out if we’re shutdown or
// errors occur unexpectedly.
if (n <= 0) break;
len = ntohl (len); // Convert byte-ordering.
if (len > LOG_RECORD_MAX) break;
// The second then reads
// bytes to obtain the actual record.
// This code handles "short-s".
n = stream.recv_n (log_record, len);
// Bail out if we’re shutdown or
// errors occur unexpectedly.
if (n <= 0) break;
{
// Constructor of Guard automatically
// acquires the lock.
Guard mon (lock);
// Execute following two statements in a
// critical section to avoid race conditions
// and scrambled output, respectively.
++request_count; // Count # of requests
if (write (STDOUT, log_record, len) == -1)
break;
// Destructor of Guard automatically
// releases the lock, regardless of
// how we exit this block!
}
}
// Destructor of automatically
// closes down .
return 0;
}
注意上面的代码是怎样解决2.2.2所示代码的各种问题的。例如,SOCK_Stream和Guard的析构器会分别关闭socket句柄和释放Thread_Mutex,而不管代码块是怎样退出的。同样地,此代码要容易移植和维护得多,因为它没有使用平台特有的API。
本论文中的例子聚焦于并发网络编程。但是,包装外观模式已被应用到其他的许多领域,比如GUI构架和数据库类库。下面是包装外观模式的一些广为人知的应用:
Microsoft Foundation Class(MFC):MFC提供一组封装大多数低级C Win32 API的包装外观,主要集中于提供实现Microsoft文档/模板体系结构的GUI组件。
ACE构架:2.2.8描述的互斥体、线程和socket的包装外观分别基于ACE构架中的组件[7]:ACE_Thread_Mutex、ACE_Thread_Manager和ACE_SOCK*类。
Rogue Wave类库:Rogue Wave的Net.h++和Threads.h++类库在许多OS平台上实现了socket、线程和同步机制的包装外观。
ObjectSpace System :该工具包也提供了socket、线程和同步机制的包装外观。
Java虚拟机和Java基础类库:Java虚拟机(JVM)和各种Java基础类库,比如AWT和Swing,提供了一组封装大多数低级的本地OS系统调用和GUI API的包装外观。
包装外观模式提供以下好处:
更为简洁和健壮的编程接口:包装外观模式在一组更为简洁的OO类方法中封装许多低级函数。这减少了使用低级函数和数据结构开发应用的枯燥性,从而降低了发生编程错误的潜在可能性。
改善应用可移植性和可维护性:包装外观类的实现可用以使应用开发者与低级函数和数据结构的不可移植的方面屏蔽开来。而且,通过用基于逻辑设计实体(比如基类、子类,以及它们的关系)的应用配置策略取代基于物理设计实体(比如文件和#ifdef)的策略[6],包装外观模式改善了软件结构。一般而言,根据应用的逻辑设计、而不是物理设计来理解和维护它们要更为容易一些。
改善应用的模块性、可复用性和可配置性:通过使用像继承和参数化类型这样的OO语言特性,包装外观模式创建的可复用类组件可以一种整体方式被“插入”其他组件,或从中“拔出”。相反,不求助于粗粒度的OS工具,比如链接器或文件系统,替换成组的函数要难得多。
包装外观模式有以下缺点:
额外的间接性(Indirection):与直接使用低级的函数和数据结构相比,包装外观模式可能带来额外的间接。但是,支持内联的语言,比如C++,可以无需显著的开销而实现该模式,因为编译器可以内联用于实现包装外观的方法调用。
包装外观模式与外观模式是类似的[8]。外观模式的意图是简化子系统的接口。包装外观模式的意图则更为具体:它提供简洁、健壮、可移植和可维护的类接口,封装低级的函数和数据结构,比如本地OS互斥体、socket、线程和GUI C语言API。一般而言,外观将复杂的类关系隐藏在更简单的API后面,而包装外观将复杂的函数和数据结构关系隐藏在更丰富的类API后面。
如果动态分派被用于实现包装外观方法,包装外观模式可使用桥接模式[8]来实现;包装外观方法在桥接模式中扮演抽象(Abstraction)角色。
本论文描述包装外观模式,并给出了详细的例子演示怎样使用它。在本论文中描述的ACE包装外观组件的实现可在ACE[7]软件发布中自由获取(URL:http://www.cs.wustl.edu/~schmidt/ACE.html)。该发布含有在圣路易斯华盛顿大学开发的完整的C++源码、文档和测试例子驱动程序。目前ACE正在用于许多公司(像Bellcore、波音、DEC、爱立信、柯达、朗讯、摩托罗拉、SAIC和西门子)的通信软件项目中。
感谢Hans Rohnert、Regine Meunier、Michael Stal、Christa Schwanninger、Frank Buschmann和Brad Appleton,他们的大量意见极大地改善了包装外观模式描述的形式和内容。
[1] F. Buschmann, R. Meunier, H. Rohnert, P. Sommerlad, and M. Stal, Pattern-Oriented Software Architecture - A System of Patterns. Wiley and Sons, 1996.
[2] W.R.Stevens,UNIX Network Programming, First Edition. Englewood Cliffs, NJ: Prentice Hall, 1990.
[3] J. Eykholt, S. Kleiman, S. Barton, R. Faulkner, A. Shivalin-giah, M. Smith, D. Stein, J. Voll, M. Weeks, and D. Williams, “Beyond Multiprocessing... Multithreading the SunOS Ker-nel,” in Proceedings of the Summer USENIX Conference,(San Antonio, Texas), June 1992.
[4] W.R.Stevens,UNIX Network Programming, Second Edition. Englewood Cliffs, NJ: Prentice Hall, 1997.
[5] D. C. Schmidt, “IPC SAP: An Object-Oriented Interface to Interprocess Communication Services,” C++ Report,vol.4, November/December 1992.
[6] J. Lakos, Large-scale Software Development with C++. Reading, MA: Addison-Wesley, 1995.
[7] D. C. Schmidt, “ACE: an Object-Oriented Framework for Developing Distributed Applications,” in Proceedings of the 6th USENIX C++ Technical Conference, (Cambridge, Mas-sachusetts), USENIX Association, April 1994.
[8] E. Gamma, R. Helm, R. Johnson, and J. Vlissides, Design Pat-terns: Elements of Reusable Object-Oriented Software. Read-ing, MA: Addison-Wesley, 1995.
[9] D. C. Schmidt, “Acceptor and Connector: Design Patterns for Initializing Communication Services,” in Pattern Languages of Program Design (R. Martin, F. Buschmann, and D. Riehle, eds.), Reading, MA: Addison-Wesley, 1997.
[10] H. Mueller, “Patterns for Handling Exception Handling Suc-cessfully,” C++ Report, vol. 8, Jan. 1996.
[11] D. C. Schmidt, T. Harrison, and N. Pryce, “Thread-Specific Storage – An Object Behavioral Pattern for Accessing per-Thread State Efficiently,” C++ Report,vol.9,Novem-ber/December 1997.
[12] Bjarne Stroustrup, The C++ Programming Language, 3rd Edition. Addison-Wesley, 1998.
