这一部分检查一个使用实例,以演示在C++包装中封装Solaris并发机制的优点。该使用实例描述了一个基于生产系统的有代表性的情景[25]。紧跟4.5对库接口的介绍,在4.6中还有ACE OO线程封装类库的其他例子。
通过归纳系统开发中发生的实际问题的解决方案,许多有用的C++类已逐渐发展起来。但是在类的接口和实现稳定后,随着时间的过去,这样的反复对类进行归纳的过程已不再被强调。这让人遗憾,因为要进入面向对象设计和C++的主要障碍是(1)学习并使怎样识别和描述类和对象的过程内在化,以及(2)理解何时以及怎样应用(或不应用)像模板、继承、动态绑定和重载这样的C++特性来简化和归纳他们的程序。
为努力把握C++类设计演变的动力特性,下面的部分演示逐步应用面向对象技术和C++习语、以解决一个惊人地微妙的问题的过程。该问题发生在开发并发分布式应用族的过程中,该应用族在单处理器和多处理器平台上都能高效地执行。通过使用模板和重载来透明地将同步机制参数化进并发应用,这一部分集中考察那些涉及对已有代码进行归纳的步骤。其基础代码基于在[1, 26, 20]中描述的自适配通信环境(ACE)构架中的组件。
此例子检查若干C++的语言特性,它们可以更为优雅地解决4.3.5.1提出的序列化问题。如在那里所描述的,原来的方案既不优雅、不可移植、易错,并且还要求强制性地改变源代码。这一部分演示一种进化的、在先前反复的设计演变中所产生的洞见之上构建的C++方案。
解决原来问题的一种更为优雅一点的方案是通过C++ Thread_Mutex包装来封装现有的Solaris mutex_t操作,如下所示:
class Thread_Mutex
{
public:
Thread_Mutex (void)
{
mutex_init (&lock_, USYNC_THREAD, 0);
}
?Thread_Mutex (void)
{
mutex_destroy (&lock_);
}
int acquire (void)
{
return mutex_lock (&lock_);
}
int release (void)
{
return mutex_unlock (&lock_);
}
private:
// Solaris 2.x serialization mechanism.
mutex_t lock_;
};
给互斥机制定义C++包装接口的一个优点是应用代码现在变得更为可移植了。例如,下面是Thread_Mutex类接口的实现,它基于Windows NT WIN32 API[4]中的机制之上:
class Thread_Mutex
{
public:
Thread_Mutex (void)
{
InitializeCriticalSection (&lock_);
}
?Thread_Mutex (void)
{
DeleteCriticalSection (&lock_);
}
int acquire (void)
{
EnterCriticalSection (&lock_); return 0;
}
int release (void)
{
LeaveCriticalSection (&lock_); return 0;
}
private:
// Win32 serialization mechanism.
CRITICAL_SECTION lock_;
};
使用Thread_Mutex C++包装类使原来的代码变得更为清晰,改善了可移植性,并且确保了Thread_Mutex对象被定义时,初始化工作自动地进行。如下面的代码段所示:
例4-3
typedef u_long COUNTER;
// At file scope.
static COUNTER request_count;
// Thread_Mutex protecting request_count.
static Thread_Mutex m;
void *run_svc (void *)
{
for (int i = 0; i < iterations; i++)
{
m.acquire ();
// Count # of requests.
++request_count;
m.release ();
}
return (void *) iterations;
}
但是,C++封装方法并没有解决所有在4.3.5.1所标出的问题。特别地,它没有解决忘记释放互斥体的问题(它还是需要程序员的人工干预)。此外,使用Thread_Mutex也还是需要对原来的非线程安全的代码进行强制性的改变。
确保锁被自动释放的一种直截了当的方法是使用C++类构造器和析构器语义。下面的工具类使用这些语言构造来自动进行互斥体的获取和释放:
class Guard
{
public:
Guard (const Thread_Mutex &m): lock_ (m)
{
lock_.acquire ();
}
?Guard (void)
{
lock_.release ();
}
private:
const Thread_Mutex &lock_;
}
Guard定义了一“块”代码,在其上一个Thread_Mutex被自动获取,并在退出代码块时自动释放。它采用了一种通常称为“作为资源获取的构造器椬魑试词头诺奈龉蛊鳌?/FONT>[9, 27, 7]的C++习语。
如上面的代码所示,当Guard类的对象被创建时,它的构造器自动获取Thread_Mutex对象上的锁。同样地,Guard类的析构器在对象出作用域时自动解锁Thread_Mutex对象。
注意Guard类的数据成员lock_是Thread_Mutex对象的一个引用。这避免了在Guard对象的构造器和析构器每次执行时创建和销毁底层Solaris mutex_t变量的开销。
通过对代码作出轻微的变动,我们现在保证了Thread_Mutex被自动获取和释放:
例4-4
typedef u_long COUNTER;
// At file scope.
static COUNTER request_count;
// Thread_Mutex protecting request_count.
static Thread_Mutex m;
void *run_svc (void *)
{
for (int i = 0; i < iterations; i++)
{
{
// Automatically acquire the mutex.
Guard monitor (m);
++request_count;
// Automatically release the mutex.
}
// Remainder of service processing omitted.
}
}
但是,该方案还是没有解决强制性改变代码的问题。而且,在Guard周围增加额外的‘{’和‘}’花括号分隔符块既不优雅又容易出错。进行维护的程序员可能会误认为花括号并不重要并将它们去除,产生出下面的代码:
for (int i = 0; i < iterations; i++)
{
Guard monitor (m);
++request_count;
// Remainder of service processing omitted.
}
遗憾的是,这样的“花括号省略”有副作用:它通过序列化主事件循环、消除了应用中所有并发执行。因此,所有应该在那段代码区中并行执行的计算都会被不必要地序列化。
要以一种透明的、非强制的和高效的方式解决现存的问题,需要使用两种另外的C++特性:参数化类型和操作符重载。我们使用这些特性来提供一个称为Atomic_Op的模板类,其部分代码显示如下(完整的接口见4.5.6.2):
template <class TYPE>
class Atomic_Op
{
public:
Atomic_Op (void) { count_ = 0; }
Atomic_Op (TYPE c) { count_ = c; }
TYPE operator++ (void)
{
Guard monitor (lock_);
return ++count_;
}
operator TYPE ()
{
Guard monitor_ (lock_);
return count_;
}
// Other arithmetic operations omitted...
private:
Thread_Mutex lock_;
TYPE count_;
};
Atomic_Op类重新定义了普通的针对内建数据类型的算术操作符(比如++、--、+=,等等),以使这些操作符原子地工作。一般而言,由于C++模板的“延期实例化”语义,任何定义了基本算术操作符的类都将与Atomic_Op类一起工作。
因为Atomic_Op类使用了Thread_Mutex的互斥特性,针对Atomic_Op的实例化对象的算术运算现在在多处理器上工作正常。而且,C++特性(比如模板和操作符重载)还允许这样的技术在多处理器上透明地工作。此外,Atomic_Op中的所有方法操作都被定义为内联函数。因此,一个C++优化编译器将生成代码确保Atomic_Op的运行时性能不会低于直接调用mutex_lock和mutex_unlock函数。
使用Atomic_Op类,我们现在可以编写下面的代码,几乎等同于原来的非线程安全代码(实际上,只是改变了COUNTER的类型定义):
例4-5
typedef Atomic_Op<u_long> COUNTER;
// At file scope
static COUNTER request_count;
void *run_svc (void *)
{
for (int i = 0; i < iterations; i++)
{
// Actually calls Atomic_Op::operator++()
++request_count;
}
}
通过结合C++构造器/析构器习语(以自动获取和释放Thread_Mutex)和模板及重载的使用,我们生成了一种既简单又非常有表现力的参数化类抽象。该抽象可在无数需要原子操作的类型族上正确而原子地运作。例如,要为其他算术类型提供同样的线程安全功能,我们只需简单地实例化Atomic_Op模板类的新对象:
Atomic_Op<double> atomic_double;
Atomic_Op<Complex> atomic_complex;
尽管上面描述的Atomic_op和Guard类的设计产生了正确和透明的线程安全程序,还是存在着足资改进的空间。特别地,注意Thread_Mutex数据成员的类型是被硬编码进Atomic_Op类的。既然在C++中可以使用模板,这样的设计决策就是一种不必要的、可以轻易克服的限制。解决方案就是参数化Guard,并增加另一种类型参数给模板类Atomic_Op,如下所示:
template <class LOCK>
class Guard
{
// Basically the same as before...
private:
// new data member change.
const LOCK &lock_;
};
template <class LOCK, class TYPE>
class Atomic_Op
{
TYPE operator++ (void)
{
Guard<LOCK> monitor (lock_);
return ++count_;
}
// ...
private:
LOCK lock_; // new data member
TYPE count_;
};
使用这个新类,我们可以在源代码的开始处作出下面的简单变动:
typedef Atomic_Op <Thread_Mutex, u_long> COUNTER;
// At file scope.
COUNTER request_count;
// ... same as before