贝贝花花包包店,精品555皮具,钱夹,皮夹

字体: | 推荐给好友 上一篇 | 下一篇

ACE技术论文集-第5章 C/C++线程专有存储(Thread-Specific Storage)

发布: 2008-6-13 14:50 | 作者: Prashant Jain and D | 来源: 转载 | 查看: 483次

5C/C++线程专有存储(Thread-Specific Storage):用于访问“per-Thread”状态的对象行为模式
 
Douglas C. Schmidt Nat Pryce Timothy H. Harrison
 
在理论上,使应用多线程化可以改善性能(通过同时执行多个指令流),并简化程序结构(通过允许每个线程同步地、而不是反应式地或异步地执行)。而在实践中,由于获取和释放锁的开销,多线程应用常常并不比单线程应用执行得更好,甚至还会更糟。此外,由于避免条件竞争和死锁所需的复杂的并发控制协议,多线程应用常常难以编程。
本论文描述线程专有存储(Thread-Specific Storage)模式,该模式可减轻多线程性能和编程复杂性的若干问题。通过允许多个线程使用一个逻辑上的全局访问点来获取线程专有数据,而又不给每次访问带来锁定开销,线程专有存储模式可改善性能,并简化多线程应用。
 
5.1意图
 
允许多个线程使用一个逻辑上的全局访问点来获取线程专有数据,而又不给每次访问带来锁定开销。
 
5.2动机
 
5.2.1上下文和压力
 
线程专有存储应被用于这样的多线程应用:它们经常访问那些逻辑上是全局的、而物理上是专有于每个线程的对象。例如,像UNIX和Win32这样的操作系统使用errno来向应用报告错误信息。当错误在系统调用中发生时,OS设置errno来报告问题、并返回文档化的失败状态。当应用检测到失败时,它检查errno来确定发生了何种类型的错误。
例如,考虑下面典型的C代码片段,它接收来自非阻塞TCP socket的缓冲区:
 
// One global errno per-process.
extern int errno;
 
void *worker (SOCKET socket)
{
// Read from the network connection
// and process the data until the connection
// is closed.
for (;;)
{
char buffer[BUFSIZ];
int result = recv (socket, buffer, BUFSIZ, 0);
 
// Check to see if the recv() call failed.
if (result == -1)
{
if (errno != EWOULDBLOCK)
// Record error result in thread-specific data.
printf ("recv failed, errno = %d", errno);
}
else
// Perform the work on success.
process_buffer (buffer);
}
}
 
如果recv返回-1,代码检查errno是否等于 EWOULDBLOCK,如果不是(例如,如果errno = EINTR)就打印出错误消息;返回的不是-1代码就处理它接收到的缓冲区。
 
5.2.2常见陷阱和缺陷
 
尽管上面所示的“全局错误变量”方法对于单线程应用工作得相当好,在多线程应用中却会发生微妙的问题。特别地,占先式多线程系统中的条件竞争会导致一个线程中的方法所设置的errno值被另一线程中的应用错误地解释。因而,如果多个线程同时执行worker函数,全局版本的errno就有可能会由于条件竞争而被不正确地设置。
例如,在图5-1中两个线程(T1和T2)可以在socket上执行recv调用。在此例中,T1的recv返回-1,并设置errno为EWOULDBLOCK,指示目前没有数据在socket上排队。但是在它检查这种情况之前,T1被占先,T2运行。假设T2被中断,它设置errno为EINTR。如果T2随之又立即被占先,T1将会错误地假定它的recv调用被中断,并执行错误的动作。因而,这个程序是错误的和不可移植的,因为它的行为依赖于线程执行的顺序。
在这之下的问题是对全局errno值的设置和测试发生在两个步骤中:(1)recv调用设置该值和(2)应用测试该值。因此,“显而易见”的解决方案,即用互斥体包装errno并不能解决竞争状态,因为设置/测试涉及到多个操作(也就是,它不是原子的)。
解决此问题的一种途径是创建更为成熟的锁定协议。例如,recv调用可以在内部获取errno_mutex,并且必须由应用在recv返回、errno的值被测试后来释放它。但是,此方案并不合乎需要,因为应用可能会忘记释放锁,从而导致饥饿和死锁。而且,如果应用必须在每次库调用后检查错误状态,额外的锁定开销将会显著地降低性能,即使在没有使用多个线程的情况下也是如此。
 
 
图5-1 多线程程序中的条件竞争
 
5.2.3解决方案:线程专有存储
 
上面描述的陷阱和缺陷的一种通用解决方案是使用线程专有存储模式。该模式消除以下压力:
 
  • 效率:线程专有存储允许线程中的一系列方法原子地访问线程专有的对象,而又不会给每次访问带来锁定开销。
  • 简化应用编程:对于应用程序员来说,线程专有存储易于使用,因为系统开发者可以通过数据抽象或宏来使线程专有存储的使用在源码级完全透明化。
  • 高度可移植:线程专有存储在大多数多线程OS平台上都可用,并且可以在缺乏它的平台上(比如VxWorks)方便地实现。
 
因此,不管应用是运行在单线程还是多线程中,使用线程专有存储模式都不会带来额外开销,并且无需改动代码。例如,下面的代码演示errno是如何在Solaris 2.x上定义的:
 
// A thread-specific errno definition (typically
// defined in).
 
#if defined (_REENTRANT)
// The _errno() function returns the
// thread-specific value of errno.
#define errno (*_errno())
#else
// Non-MT behavior is unchanged.
extern int errno;
#endif /* REENTRANT */
 
void *worker (SOCKET socket)
{
// Exactly the same implementation shown above.
}
 
如果_REENTRANT标志被设置,errno符号就被定义为调用名为_errno的助手函数的宏,此函数返回一个指向errno的线程专有值的指针。该指针被宏去除引用,以使它能够任意出现在赋值运算的左边或右边。
 
5.3适用性
 
应用有以下特性时可使用线程专有存储:
 
  • 应用最初的编写假定了单线程控制,并正在被移植到多线程环境,而又不能改变现有API;或是
  • 应用含有多个占先式线程控制,可以任意的调度顺序并发执行;以及
  • 每个线程控制调用一系列方法,这些方法共享只对该线程来说是公用的数据;以及
  • 在每个线程中被对象共享的数据必须通过一个全局可见的访问点来访问;该访问点“逻辑地”与其他线程共享,但在“物理上” 对于每个线程却是唯一的;以及
  • 数据在方法间隐式地传递,而不是经由参数显式地传递。
 
理解上面描述的特性对于使用(或不使用)线程专有存储模式来说是至关紧要的。例如,UNIX errno变量是一个数据例子:(1)逻辑上全局,但是物理上线程专有,以及(2)在方法间隐式地传递。
 
当应用有以下特性时,不要使用线程专有存储模式:
 
  • 多个线程为单个任务协同工作,该任务需要并发访问共享数据。例如,多线程应用可以对在内存中的数据库并发地进行读写。在这样的情况下,线程必须共享不是线程专有的记录和表。如果使用线程专有存储来存储此数据库,线程就不能共享这些数据。因而,对数据库记录的访问必须通过同步原语(例如,互斥体)来控制,以使线程能在共享数据上协作。
  • 维护物理逻辑上都分离的数据要更为直观和高效。例如,通过将数据作为参数显式地传递给所有方法,有可能使线程访问仅在每个线程中可见的数据。在这样的情况下,线程专有存储模式有可能是不必要的。
 
5.4结构和参与者
 
图5-2演示线程专有存储中的以下参与者的结构:
 
应用线程(Application Thread
 
  • 应用线程使用TS Object Proxy(TS对象代理)来访问驻留在线程专有存储中的TS Object。如5.9所示,线程专有存储模式的实现可以使用“灵巧指针”(smart pointer)来隐藏TS Object Proxy,以使应用看起来像是在直接访问TS Object。
 
 
图5-2 线程专有存储模式中的参与者的结构
 
线程专有对象代理(TS Object Proxy)(errno宏)
 
  • TS Object Proxy定义TS Object的接口。它负责通过getspecific和setspecific方法来为每个应用线程提供对一个唯一的对象的访问。例如,在5.2的错误处理例子中,errno TS Object是一个int。
    一个TS Object Proxy的实例负责一种类型的对象,也就是,它为所有访问代理的线程而充当对线程专有TS Object进行访问的中介。例如,多个线程可以使用同一个TS Object Proxy来访问线程专有的errno值。代理所存储的key(专有钥)值是由TS Object Collection(TS对象集合)在代理被创建时分配的,并由getspecific和setspecific方法传递给集合。
    TS Object Proxy的目的是隐藏key和TS Object Collection。没有代理,Application Thread必须显式地获取集合和使用专有钥。如5.9所示,线程专有存储的大多数细节可以通过TS Object Proxy的灵巧指针来完全隐藏。
 
线程专有对象(TS Object)(* _errno()value
 
  • TS Object是特定线程的线程专有对象的实例。例如,线程专有的errno是int类型的对象。它由TS Object Collection管理,并且只能通过TS Object Proxy来访问。
 
线程专有对象集合(TS Object Collection
 
  • 在复杂的多线程应用中,线程的errno值可以是驻留在线程专有存储中的许多类型的数据中的一种。因而,要获取线程专有错误数据的线程,必须使用一个key。这个key必须与errno相关联,以允许线程访问TS Object Collection中的正确条目。
    TS Object Collection含有一组与特定线程相关联的所有线程专有对象,也就是,每个线程都有唯一的TS Object Collection。TS Object Collection将key映射到线程专有的TS Object。通过get_object(key)和set_object(key)方法,TS Object Proxy使用key来从TS对象集合中获取一个特定的TS对象。

51/512345>
 

评分:0

我来说两句

seccode