5.5线程特定的存储器(Thread-Specific Storage)
1.问题
为了避免竞争条件、资源耗尽和死锁多线程应用程序需要复杂的并发控制协议,从而难以编程。由于存在加锁开销,所以多线程应用程序的性能往往比不上单线程应用程序,事实上它们的性能可能更糟,特别是在多处理平台上。在并发程序中有两个强制条件:
1)多线程应用程序应既容易编程又高效。特别地,对逻辑上全局的但物理上局限于一个线程的数据的访问应是原子的,且不会导致对每次访问的加锁开销。
2)许多遗产库和应用程序最初是在单一控制线程的假定下编写的。因此它们常常在方法间通过诸如errno的全局对象隐式地传递数据,而不是显式地传递参数。将这些代码转移到多线程中运行时,通常不太可能改变遗产应用程序中现存的接口和代码。
2.解决方案
为每个针对具体线程的对象引入一个全局访问点,但是在每个线程的存储器中保存“真实”的对象。应用程序仅通过它们的全局访问点管理这些线程特定对象。
3.结构
线程特定对象是一个只能由特定线程访问的对象实例。
线程使用一个由关键字工厂分配的关键字来标识一个线程特定对象。由关键字工厂生成的关键字有一个取值范围,以确保每个线程特定对象在“逻辑上”是全局的。
一个线程特定对象包含与特定线程关联的线程特定对象的集合。每个线程有自己的线程特定对象集。在线程特定对象集内部定义了一对方法,set()和get(),将全局管理的关键字集合映射到集合中存储的线程特定对象。通过向get()传递一个标识对象的关键字作为参数,线程特定对象集的客户端可以获得一个指向某一线程特定对象的指针。客户机可以通过由get()方法返回的指针检查或修改对象。类似地,客户机可以通过将对象指针以及相关的关键字以参数形式传递给set()来向对象集添加一个指向线程特定对象的指针。
可以定义一个线程特定对象代理,使客户机像访问普通对象一样访问一个特定类型的线程特定对象。如果不使用代理,那么客户机必须直接访问线程特定对象集并显式地使用关键字,这样做单调乏味并且易出错。每个代理实例存储一个惟一区分线程特定对象的关键字,这样,每个关键字和每个线程都对应一个线程特定对象。
线程特定对象代理与相关的线程特定对象的接口一致。在内部,代理的接口方法首先使用由线程特定对象集提供的set()和get()方法,获得一个由被存储在代理中的关键字指定的指向线程特定对象的指针。在指针被获得后,代理将最初的方法调用委托给它。
应用程序线程是客户机,它使用线程特定对象代理访问驻留在线程特定的存储器中的线程特定对象。对于应用程序线程而言,当对一个线程特定对象进行实际方法调用时,该方法看上去好像是在对一个普通对象进行调用。多应用线程可以使用相同的线程特定对象代理访问它们惟一的线程特定对象。代理使用调用它的接口方法的应用线程的标识符,来区分它所封装的线程特定对象。
4.实现
1)实现线程特定对象集。
1.1)确定线程特定对象的类型。
1.2)确定将线程特定对象集存储在何处。每个线程特定对象集既可以存储在所有线程外也可以存储在线程内:
·在所有线程外部。该策略将每个应用程序线程的标识符映射为一个存储在所有线程外部的全局线程特定对象集表。注意,应用程序线程可以通过调用线程库中的API函数获得它自身的线程标识符。因此,外部线程特定对象集的实现可以容易地确定哪个线程特定对象集与指定应用程序线程相关联。
取决于外部表策略的不同实现,线程可以访问其他线程中的线程特定对象集。如果线程特定的存储器实现可以在不再需要关键字时回收它们。这也许是有用的,因为全局表有助于由“清除”线程访问所有线程特定对象集以删除对应于被回收关键字的条目,关键字的回收对于仅支持有限数量关键字的线程特定的存储器模式实现特别有用。
将线程特定对象集存储在线程外部的全局跟中的缺点是,增加了访问每个线程特定对象的开销。该开销是由每次修改包含所有线程特定对象集的全局表时,为避免竞争条件而使用的同步机制所引起的。特别地,当关键字工厂建立一个新关键字时,需要进行串行化,因为其他应用程序线程可能在并发地建立关键字。然而,在相应线程特定对象集被确定后,应用程序线程不需要进行任何加锁操作就可以访问集合中的线程特定对象。
·在每个线程内部。这种策略要求每个线程连同它的其他内部状态,如它的运行时线程栈、程序计数器、通用寄存器和线程标识符,一同存储在线程特定对象集中。当线程访问一个线程特定对象时,使用相应的关键字作为到线程内部的线程特定对象集的索引,从而获取该对象。和上述外部策略不同,当线程特定对象集存储在每个线程内部时,不需要进行串行化。在这种情况下,所有对线程内部状态的访问都发生在线程内部。
然而,虽然不同消耗更多的总内存,但在每个线程本地存储线程特定对象集时每个线程需要更多状态。只要大小的增长不会导致线程建立,语境切换或销毁的开销的显著增长,线程特定对象的内部策略就比外部策略更有效。
1.3)定义一个将应用程序线程标识符映射到线程特定对象集的数据结构。
1.4)定义在线程特定对象集中将关键字映射到线程特定对象的数据结构。
1.5)定义关键字工厂。
1.6)定义从线程特定对象集存储和获得线程特定对象的方法。
2)实现线程特定对象代理。线程特定的存储器模式定义了一个线程特定对象代理。每个代理应用代理模式定义一个对象作为线程特定对象的“代理”。在代理上调用方法的应用线程看起来,就像是访问一个普通对象,而实际上代理将方法传递给线程特定对象。该设计防止应用程序了解线程特定的存储器的使用时间和方式。它也允许应用程序使用高层的、类型安全的以及平台无关的包装器外观访问由低层C函数API管理的线程特定对象。
2.1)定义线程特定对象代理接口。对于线程特定对象,有两个设计代理接口的策略,多态和参数化类型。
2.2)实现线程特定对象代理的建立和析构。不论是应用多态还是参数化类型策略定义线程特定对象代理,都必须对线程特定对象代理的建立和析构进行管理。一般来说,代理的构造函数内部并不分配关键字或新的线程特定对象实例,有两个原因:
·线程特定的存储器语义。不同于使用代理的线程,线程特定对象代理通常由一个线程(例如应用程序的主线程)来创建。因此,在构造函数中,预初始化一个新的线程特定对象并不能获得什么好处,因为该实例只能由创建它的线程访问。
·延迟建立。在某些操作系统中,关键字资源是有限的,而且应该直到绝对需要时才被分配。关键字的建立因此应当延迟,直到代理方法第一次调用。
线程特定对象代理的析构函数提供了几个富有技巧的设计。“显而易见的”解决方案是释放由关键字工厂分配的关键字。然而,该方法有几个问题:
·不可移植性。很难编写一个可移植的释放关键字的代理析构函数。例如,Solaris线程(与Win32和POSIX Pthreads不同)没有释放不需要的关键字的API。
·竞争条件。Solaris线程不提供释放关键字的API的原因是它很难有效和正确地实现。问题在于每个线程都保持关键字所引用对象的独立的拷贝。只有所有线程都退出,并且内存被回收后才能安全地释放一个关键字。
2.3)实现对线程特定对象的访问。
使用多态策略时,具体代理的接口必须包括所有该类中线程特定对象提供的方法。具体代理的方法实现通常执行如下四个步骤:
·如果还没有创建这种线程特定对象的话,创建一个新关键字。必须防止多线程为同一个TYPE的线程特定对象同时创建新关键字,并因此可以避免竞争条件。可以通过应用双检查加锁优化模式解决该问题。
·下一步,方法必须使用由代理存储的关键字,以通过线程特定对象集获得线程特定对象。
·如果对象还不存在,则“按需”建立该对象。
·所请求的操作被转发到线程特定对象,所有操作结果都被返回给客户机应用线程。
使用参数化类型实例化一个一般代理时,可以使用智能指针和扩展接口模式策略,实现访问任何线程特定对象方法的通用机制,和多态策略一样,通用访问机制必须遵循上述实现步骤。
5.结论
优点:
1)高效。可以实现线程特定的存储器模式,使得访问线程特定的数据时不需要加锁。
2)可重用性。通过应用包装器外观模式,并将可重用的线程特定的存储器模式代码与应用程序特定的类相分离,可以使开发者避免考虑复杂并且不可移植的线程特定的关键字的建立和分配逻辑。
3)易用性。
4)可移植性。
不足:
1)它鼓励对(线程特定的)全局对象的使用。许多应用程序不需要多线程通过公共访问点访问线程特定的数据。在这种情况下,应该存储数据以便只有拥有数据的线程才能访问它。
2)它使系统结构变得模糊。线程特定的存储器的使用由于模糊了组件间的关系,因此可能使应用程序更难理解。
3)它限制了实现方式的选择。