5.9.2.1 C++委托操作符(Delegation Operator)
通过重载C++委托操作符(操作符->),应用可以调用TSS代理上的方法,就好像是在调用目标类一样。在此实现中使用的C++委托操作符控制所有对类TYPE的线程专有对象的访问。操作符->方法受到了来自C++编译器的特殊对待。如5.9.2.3所述,它先从线程专有存储那里获取一个指向适当的TYPE的指针,随后就重新委托原来在其上调用的方法。
TSS类中的大多数工作都在下面所示的操作符->方法中执行:
复制内容到剪贴板
代码:
template <class TYPE> TYPE *
TSS<TYPE>::operator-> ()
{
TYPE *tss_data = 0;
// Use the Double-Checked Locking pattern to
// avoid locking except during initialization.
// First check.
if (this->once_ == 0)
{
// Ensure that we are serialized (constructor
// of Guard acquires the lock).
Guard <Thread_Mutex> guard (this->keylock_);
// Double check
if (this->once_ == 0)
{
pthread_key_create (&this->key_, &this->cleanup_hook);
// *Must* come last so that other threads
// don’t use the key until it’s created.
this->once_ = 1;
}
// Guard destructor releases the lock.
}
// Get the data from thread-specific storage.
// Note that no locks are required here...
pthread_getspecific (this->key_, (void **) &tss_data);
// Check to see if this is the first time in
// for this thread.
if (tss_data == 0)
{
// Allocate memory off the heap and store
// it in a pointer in thread-specific
// storage (on the stack...).
tss_data = new TYPE;
// Store the dynamically allocated pointer in
// thread-specific storage.
pthread_setspecific (this->key_, (void *) tss_data);
}
return tss_data;
}
TSS模板是一个代理,它透明地将普通C++类转换为类型安全、线程专有的类。它结合了操作符->和其他一些C++特性,像模板、内联和重载。它还利用了像双重检查锁定优化[5]和代理[6, 7]这样的模式。
在代码中,双重检查锁定优化模式用于在操作符->中两次测试once_标志。尽管多个线程可以同时访问TSS的同一实例,仅有一个线程可以合法地创建一个专有钥(也就是,通过pthread_key_create)。随后所有线程将使用这一专有钥来访问参数化类TYPE的线程专有对象。因此,操作符->使用Thread_Mutex keylock_来确保仅有一个线程执行pthread_key_create。
第一个获取keylock_的线程设置once_为1,所有调用操作符->的后续线程将发现once != 0,于是跳过初始化步骤。对once_的第二次测试处理这样的情况:在第一个线程设置once_为1之前,多个并行执行的线程在keylock_处排队等候。在这种情况下,当其他排队等待的线程最终获得互斥体keylock_,它们会发现once_等于1,就不会执行pthread_key_create。
一旦key_被创建,不再需要有进一步的锁定来访问线程专有数据。这是因为pthread_{getspecific, setspecific}函数从调用线程的状态处获取类TYPE的TS Object,而此线程状态是独立于其他线程的。
除了减少锁定开销,上面所示的类TSS的实现将应用代码与对象是专有于调用线程的这一事实屏蔽开来。为达成这一点,该实现使用了像模板、操作符重载和委托操作符(也就是,操作符->)这样的C++特性。
5.9.2.2 构造器和析构器
TSS类的构造器是很小的,它只是简单地初始化局部实例变量:
template <class TYPE>
TSS<TYPE>::TSS (void): once_ (0), key_ (0) {}
注意我们没有在构造器中分配TSS专有钥或是一个新的TYPE实例。这样设计有若干原因:
* 线程专有存储语义:最初创建TSS对象的线程(例如,主线程)常常不是使用该对象的线程(例如,工作者线程)。因此,在构造器中预先初始化一个新的TYPE并没有好处,因为此实例只能被主线程访问。
* 延期的初始化:在某些OS平台上,TSS专有钥是有限的资源。例如,Windows NT仅允许每个进程总共有64个TSS专有钥。因此,不到绝对需要的时候,不应分配专有钥。相反,初始化被延期到操作符->第一次被调用时。
TSS析构器给我们带来若干棘手的设计问题。显而易见的解决方案是在操作符->中释放所分配的TSS专有钥。但是,这一方法有若干问题:
+ 特性缺乏:Win32和POSIX pthreads定义了函数来释放TSS专有钥。但是,Solaris没有。因此,很难编写一个可移植的包装。
+ 竞争状态:Solaris线程不提供函数来释放TSS专有钥的主要原因是实现起来很昂贵。问题在于每个线程都分别维护通过同一个专有钥引用的对象。只有在所有这些线程退出、且内存被回收后才能安全地释放该专有钥。
作为上面所提到的问题的结果,我们的析构器是一个空操作。
template <class TYPE>
TSS<TYPE>::?TSS (void)
{
}
cleanup_hook是一个静态方法,它在删除其ptr参数之前,将其强制转换到适当的TYPE *。
template <class TYPE> void
TSS<TYPE>::cleanup_hook (void *ptr)
{
// This cast is necessary to invoke
// the destructor (if it exists).
delete (TYPE *) ptr;
}
这确保了在线程退出时,每个线程专有对象的析构器都会被调用。
5.9.2.3 用例
下面的方案基于C++模板包装来解决我们一直在讨论的例子:被多个工作者线程访问的线程专有存储Logger。
复制内容到剪贴板
代码:
// This is the "logically" global, but
// "physically" thread-specific logger object,
// using the TSS template wrapper.
static TSS<Logger> logger;
// A typical worker function.
static void *worker (void *arg)
{
// Network connection stream.
SOCK_Stream *stream = static_cast <SOCK_Stream *> arg;
// Read from the network connection
// and process the data until the connection
// is closed.
for (;;)
{
char buffer[BUFSIZ];
int result = stream->recv (buffer, BUFSIZ);
// Check to see if the recv() call failed.
if (result == -1)
{
if (logger->errno () != EWOULDBLOCK)
// Record error result.
logger->log ("recv failed, errno = %d", logger->errno ());
}
else
// Perform the work on success.
process_buffer (buffer);
}
}
考虑上面对logger->errno的调用。C++编译器用两个方法调用来替换这一调用。第一个是对TSS::操作符->的调用,它返回一个驻留在线程专有存储中的Logger实例。随后编译器生成第二个方法,调用前面的调用返回的Logger对象的errno方法。在这种情况下,TSS作为一个代理,允许应用访问和操作线程专有的错误值,就如同它们是平常的C++对象一样。
上面的Logger例子是一个好范例,在其中使用逻辑上的全局访问点是有益的。因为worker函数是全局的,线程要同时管理Logger对象的物理和逻辑的分离并不那么简明。相反,线程专有的Logger允许多个线程使用单个逻辑访问点来操作物理上分离的TSS对象。
5.9.2.4 评估
基于C++操作符->的TSS代理设计有以下好处:
* 最大化代码复用:通过使线程专有方法与特定的应用类(也就是,形式参数类TYPE)去耦合,不再需要重写微妙的线程专有钥的创建和分配逻辑。
* 提高可移植性:将应用移植到其他线程库(比如Win32中的TLS接口)只需要改变TSS类,而不是所有使用此类的应用。
* 更大的灵活性和透明性:将一个类变为线程专有类(或相反),仅需要改变此类对象的定义方式。这可以在编译时被决定,如下所示:
#if defined (_REENTRANT)
static TSS<Logger> logger;
#else
// Non-MT behavior is unchanged.
Logger logger;
#endif /* REENTRANT */
注意不管使用的是线程专有还是非线程专有形式的Logger,Logger的使用方式都保持不变。
5.10 已知应用
下面是线程专有存储模式的已知应用:
* 在支持POSIX和Solaris线程API的OS平台上实现的errno机制是被广泛使用的线程专有存储模式的例子[1]。此外,与Win32一起提供的C运行时库支持线程专有的errno。Win32 GetLastError/SetLastError也实现了线程专有存储模式。
* 在Win32操作系统中,窗口属于线程[8]。每个拥有窗口的线程都有一个私有的消息队列,OS会在其中放入用户接口事件。获取等待处理的下一消息的API调用使下一个消息从调用线程的消息队列中出队,而该消息队列就驻留在线程专有存储中。
* OpenGL[9]是一个用于渲染三维图形的C API。程序根据多边形来渲染图形;多边形通过反复调用glVertex函数、以传递多边形的每一顶点给库来描述。在顶点被传递给库之前设置的状态变量精确地决定OpenGL在接收到顶点时如何进行绘制。该状态在OpenGL库中、或是在图形卡上作为封装的全局变量存储。在Win32平台上,OpenGL库为每个使用该库的线程在线程专有存储中维护一组唯一的状态变量。
* 线程专有存储被用于在ACE网络编程工具包[10]中实现它的错误处理方案,该方案与5.9.2.3描述的Logger方法相类似。此外,ACE还实现了5.9.2描述的线程安全的线程专有存储模板包装。
5.11 相关模式
用线程专有存储实现的对象常常被用作“per-thread”的单体(Singleton)[7],例如,errno就是一个“per-thread”单体。但是,并非所有线程专有存储的使用都是单体,因为线程可以拥有从线程专有存储中分配的一种类型的多个实例。例如,在ACE[10]中实现的每一个Task对象都在线程专有存储中存储一个清理挂钩。
5.8所示的TSS模板类被用作代理,将库、构架和应用与OS线程库提供的线程专有存储的实现屏蔽开来。
双重检查锁定优化模式[5]通常被用于这样的应用:它们利用线程专有存储模式来避免约束线程专有存储钥的初始化顺序。
5.12 结束语
由于防止竞争状态和死锁所需的额外的并发控制协议,使现有应用多线程化常常会显著地增加软件的复杂性[11]。通过允许多线程使用一个逻辑上的全局访问点来获取线程专有数据,而又不给每次访问带来锁定代价,线程专有存储模式减轻了一些同步开销和编程复杂性。
应用线程使用TS Object Proxy来访问TS Object。代理委托TS Object Collection来获取相应于每个应用线程的对象。这确保了不同的应用线程不会共享同一个TS Object。
5.9.2显示怎样实现线程专有存储模式的TS Object Proxy,以确保线程通过强类型的C++类接口来访问只属于它们自己的数据。与其他模式(比如代理、单体和双重检查锁定)和C++语言特性(比如模板和操作符重载)相结合,可实现TS Proxy,使得使用线程专有存储模式的对象可像传统对象一样被处理。
感谢
感谢Peter Sommerlad和Hans Rohnert对本论文早期版本的富有洞察力的意见。
参考文献
[1] J. Eykholt, S. Kleiman, S. Barton, R. Faulkner, A. Shivalingiah, M. Smith, D. Stein, J. Voll, M. Weeks, and D. Williams, “Beyond Multiprocessing... Multithreading the SunOS Kernel,” in Proceedings of the Summer USENIX Conference,(San Antonio, Texas), June 1992.
[2] R. G. Lavender and D. C. Schmidt, “Active Object: an Object Behavioral Pattern for Concurrent Programming,” in Pattern Languages of Program Design (J. O. Coplien, J. Vlissides, and N. Kerth, eds.), Reading, MA: Addison-Wesley, 1996.
[3] F. Mueller, “A Library Implementation of POSIX Threads Under UNIX,” in Proceedings of the Winter USENIX Conference, (San Diego, CA), pp. 29–42, Jan. 1993.
[4] IEEE, Threads Extension for Portable Operating Systems (Draft 10), February 1996.
[5] D. C. Schmidt and T. Harrison, “Double-Checked Locking – An Object Behavioral Pattern for Initializing and Accessing Thread-safe Objects Efficiently,” in Pattern Languages of Program Design (R. Martin, F. Buschmann, and D. Riehle, eds.), Reading, MA: Addison-Wesley, 1997.
[6] F. Buschmann, R. Meunier, H. Rohnert, P. Sommerlad, and M. Stal, Pattern-Oriented Software Architecture - A System of Patterns. Wiley and Sons, 1996.
[7] E. Gamma, R. Helm, R. Johnson, and J. Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software. Reading, MA: Addison-Wesley, 1995.
[8] C. Petzold, Programming Windows 95. Microsoft Press, 1995.
[9] J. Neider, T. Davis, and M. Woo, OpenGL Programming Guide: The Official Guide to Learning OpenGL, Release 1. Addison-Wesley, Reading, MA, 1993.
[10] D. C. Schmidt, “ACE: an Object-Oriented Framework for Developing Distributed Applications,” in Proceedings of the 6th USENIX C++ Technical Conference, (Cambridge, Massachusetts), USENIX Association, April 1994.
[11] J. Ousterhout, “Why Threads Are A Bad Idea (for most purposes),” in USENIX Winter Technical Conference,(San Diego, CA), USENIX, Jan. 1996.
[12] H. Mueller, “Patterns for Handling Exception Handling Successfully,” C++ Report, vol. 8, Jan. 1996.
[13] N. Pryce, “Type-Safe Session: An Object-Structural Pattern,” in Submitted to the 2nd European Pattern Languages of Programming Conference, July 1997.
[14] J. Gosling and F. Yellin, The Java Application Programming Interface Volume 2: Window Toolkit and Applets. Addison-Wesley, Reading, MA, 1996.
附录A 可选方案
在实践中,线程专有存储通常用于为面向对象软件解决下面两种使用情况:
1. 在模块间隐式地传递信息(例如,错误信息)。
2. 将遗留的以过程风格编写的单线程软件改编到现代的多线程操作系统和编程语言。
但是,对于#1使用情况,避免使用线程专有存储常常是一个好主意,因为它可能会增强模块间的耦合并降低可复用性。例如,对于错误处理的情况,常常可以使用A.1描述的异常来避开线程专有存储。
除非重新设计,对于#2使用情况,不能不使用线程专有存储。但是在设计新软件时,常常可以使用如下所描述的异常处理、显式的组件间通信上下文或对象化线程来避开线程专有存储。
A.1 异常处理
在模块间报告错误的一种优雅方法是使用异常处理。许多现代语言,比如C++和Java,使用异常处理来作为错误报告的机制。它还被用于一些操作系统中,比如Win32。例如,下面的代码演示一种假想的OS,其系统调用会扔出异常。
void *worker (SOCKET socket)
{
// Read from the network connection
// and process the data until the connection
// is closed.
for (;;)
{
char buffer[BUFSIZ];
try
{
// Assume that recv() throws exceptions.
recv (socket, buffer, BUFSIZ, 0);
// Perform the work on success.
process_buffer (buffer);
}
catch (EWOULDBLOCK)
{
continue;
}
catch (OS_Exception error)
{
// Record error result in thread-specific data.
printf ("recv failed, error = %s", error.reason);
}
}
}
使用异常处理有若干好处:
* 它是可扩展的:现代的OO语言通过一些尽量不侵犯现有接口和使用的特性(比如使用继承来定义异常类层次)来便利异常处理策略和机制的扩展。
* 它干净地使错误处理和正常处理得以去耦合:例如,错误处理信息不是被显式地传递给操作的。而且,应用不会由于没有完成对函数返回值的检查而偶然地“忽略”了一个异常。
* 它可以是类型安全的:在强类型的语言中,比如C++和Java,异常以一种强类型的方式被扔出和捕捉,以增强错误处理代码的组织和正确性。与显式地检查线程专有错误值相反,编译器会确保对于每种类型的异常,执行正确的处理方法。
但是,使用异常处理也有若干缺点:
* 它并非普遍可用的:并非所有语言都提供异常处理,而许多C++编译器也并没有实现异常。同样地,如果一种OS提供异常处理服务,它们必须被语言扩展支持,从而也就降低了代码的可移植性。
* 它使多种语言的使用复杂化:因为语言以不同的方式实现异常,或者根本不实现异常,在以不同语言编写的组件扔出异常时,可能会很难将它们集成在一起。使用整数值或结构来报告错误信息提供了一种普遍可用的解决方案。
* 它使资源管理复杂化:例如,由于增加了C++代码块中退出路径的数目[12],而导致这样的问题。如果语言或编程环境不支持垃圾回收,就必须付出努力来确保动态分配的对象在异常扔出时被删除。
* 它有时间和/或空间效率低下的可能性:异常处理的糟糕实现会带来时间和空间的过度开销,即使没有异常被扔出也是如此[12]。对于必须保持小而高效的嵌入系统来说,此开销可能会特别地成问题。
对于必须在多种平台上可移植地运行的系统级构架(比如内核级设备驱动器,或低级通信子系统)来说,异常处理的缺点会特别地成问题。对于这些类型的系统,一种更为可移植、高效和线程安全的处理错误的方法是定义一个错误处理器抽象,显式地维护关于操作的成功或失败的信息。
A.2 组件间通信的显式上下文
线程专有存储通常用于存储“per-thread”状态,以允许库和构架中的软件组件高效地进行通信。例如,errno被用于将错误值从被调用组件传递给调用者。同样地,对OpenGL API函数的调用传递信息给OpenGL库,并被存储在线程专有状态中。通过显式地将在组件间传递的信息表示为对象,可以避免使用线程专有存储。
如果预先已经知道组件必须为它的用户存储的信息的类型,调用线程可以创建对象,并将其作为操作的一个额外参数传递给组件。另外,组件必须创建一个对象来保持上下文信息,以响应来自调用线程的请求;并在线程对组件加以使用之前将一个标识符传给线程。这些类型的对象常被称为“上下文对象”(context object);软件组件按需创建的上下文对象常被称为“会话”(session)。
下面的错误处理方案是一个简单的例子,演示调用线程怎样创建上下文对象;该方案传递一个显式的参数给所有的操作:
void *worker (SOCKET socket)
{
// Read from the network connection and
// process the data until the connection
// is closed.
for (;;)
{
char buffer[BUFSIZ];
int result;
int errno;
// Pass the errno context object explicitly.
result = recv (socket, buffer, BUFSIZ, 0, &errno);
// Check to see if the recv() call failed.
if (result == -1)
{
if (errno != EWOULDBLOCK)
printf ("recv failed, errno = %d", errno);
}
else
// Perform the work on success.
process_buffer (buffer);
}
}
组件创建的上下文对象可以使用类型安全的会话模式(Type-Safe Session Pattern)[13]来实现。在此模式中,上下文对象存储组件所需的状态,并提供一个可被多态地调用的抽象接口。组件返回一个指向该抽象接口的指针给调用线程,后者随后调用接口的操作来使用组件。
OpenGL和Java AWT库[14](用于在诸如窗口、打印机或位图这样的设备上渲染图形)所提供接口之间的差异演示了怎样使用类型安全的会话。在AWT中,程序通过从设备请求一个GraphicsContext来在设备上进行绘制。GraphicsContext封装在设备上进行渲染所需的状态,并提供了一个接口,通过它程序可以设置状态变量,并调用绘制操作。可以动态地创建多个GraphicsContext对象,从而消除了任何保持线程专有状态的需要。
与线程局部存储和异常处理相比较,使用上下文对象有以下这些好处:
* 它更加可移植:它不需要有可能不被普遍支持的语言特性;
* 它更为高效:线程可以直接存储和访问上下文对象,而不必在线程专有存储表中进行查找。也不需要编译器构建额外的数据结构来处理异常;
* 它是线程安全的:上下文对象或会话句柄可以存储在线程的栈中,这自然是线程安全的。
但是使用由调用线程创建的上下文对象也有若干缺点:
* 它是强制性的:上下文对象必须传给所有操作,并且必须在每次操作后显式地检查。这搅乱了程序逻辑,并有可能需要改变已有的组件接口,以增加错误处理器参数。
* 增加了每次调用的开销:每次调用都会有额外开销,因为必须给每个方法调用增加一个额外的参数,而不管是否需要该对象。尽管在某些情况下,这是可以接受的,但对于执行非常频繁的方法,开销可能会是相当显著的。相反,除非发生错误,基于线程专有存储的错误处理方案就不需要被使用。
与在调用线程中创建上下文对象相比较,使用由组件创建的会话有以下好处:
* 它较少强制性:线程不必显式地将上下文对象作为操作的参数传给组件。编译器会安排将指向上下文对象的指针作为隐藏的this指针传给它的操作。
* 它使初始化和关闭自动化:在线程从组件那里获取会话之前,它不能开始使用会话。于是如果多个操作在矛盾的状态中,组件就可以确保它们不会被调用。相反,如果组件使用隐藏的状态,调用者必须在调用操作之前显式地初始化库,并在组件结束后将它关闭。忘记这样做会导致含混的错误或资源浪费。
* 结构是显式的:不同代码模块之间的关系被显式地表示为对象,这使得要理解系统的行为变得更容易。
与在调用者的栈上创建上下文对象相比较,在组件中创建它们有以下缺点:
* 分配开销:组件必须在堆上、或是从某种封装的缓存那里分配会话对象。比起在栈上分配对象,这样做的效率常常要更为低下。
A.3 对象化线程
在面向对象语言中,应用可以显式地将线程表示为对象。线程类可以通过从一个抽象基类派生来定义,在此基类中封装了作为并发线程运行所需的状态;并调用一个实例方法来作为线程的入口。线程的入口方法可以在基类中被定义为纯虚函数,并在派生类中定义。任何所需的线程专有状态(比如会话上下文)可被定义为对象实例变量,并可为线程类的任何方法所用。对这些变量的同时访问可以通过使用语言级访问控制机制、而不是显式的同步对象来防止。
下面使用ACE Task(任务)[10]的一种变种来演示这一方法;任务可用于将线程控制和对象相关联:
class Task
{
public:
// Create a thread that calls the svc() hook.
int activate (void);
// The thread entry point.
virtual void svc (void) = 0;
private:
// ...
};
class Animation_Thread : public Task
{
public:
Animation_Thread (Graphics_Context *gc)
: device_ (gc) {}
virtual void svc (void)
{
device_->clear ();
// ... perform animation loop...
}
private:
Graphics_Context *device_;
};
使用对象化线程有以下好处:
* 它更为高效:线程无需在隐藏的数据结构中进行查找、以访问线程专有状态。
* 它不具强制性:使用对象化线程时,指向当前对象的指针作为每个函数调用的额外参数被传递。不像显式的会话上下文,在源码中该参数是隐藏的,并由编译器自动管理,从而可使源码保持整洁。
使用对象化线程有以下缺点:
* 不容易访问线程专有存储:只有类方法可以访问实例变量。这使得使用实例变量来在可复用库和线程间进行通信变得不直观。无论如何,以这种方式使用线程专有存储增强了组件间的耦合。一般而言,异常提供了在模块间报告错误的一种更为弱耦合的方法,尽管在像C++这样的语言里它们有着自身的陷阱和缺陷[12]。
* 额外开销:传给每个操作的额外、隐藏的参数会带来一些开销。在执行非常频繁的函数中,这些开销可能会非常地显著。
This file is decompiled by an unregistered version of ChmDecompiler.
Regsitered version does not show this message.
You can download ChmDecompiler at :
http://www.zipghost.com/