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

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

ACE技术论文集-第4章 ACE轻量级OS并发机制的OO封装

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

 
Douglas C. Schmidt
 
摘 要
 
本论文描述ACE面向对象的线程封装C++类库的设计,该类库使程序员与Solaris线程、POSIX pthreads及Win32线程之间的差异相屏蔽;并从最终用户和内部设计的视角来展现其体系结构,并讨论了关键的设计和实现问题。读者将获得对总体设计方法,以及在多种软件质量因素,如质量、可移植性及可扩展性之间所做的权衡的理解。
 
4.1介绍
 
某些类型的分布式应用通过使用并发模式执行任务来从中获益。对于多处理器平台上的网络服务器,并发特别有助于改善性能和简化编程。对于服务器应用,使用线程来并发地处理多客户请求常常比下面的设计方法要更为方便和更不易出错:
 
  • 在传输层接口对请求进行人工的序列化;
  • 在内部对请求进行排队,并依次处理它们;
  • 为每一客户请求派生一个重量级的进程。
 
本论文描述ACE自适配通信环境[1]中包含的C++类库。ACE封装并增强了由Solaris 2.x线程[2]、POSIX Pthreads[3]及Win32线程[4]所提供的轻量级并发机制。
在本论文中介绍的材料面对的是那些有兴趣了解线程的面向对象(OO)并发编程的战略和战术的技术人员。读者被假定熟悉一般的OO设计和编程技术(比如设计模式[5]、应用构架[6]、模块性、信息隐藏和对象建模[7])、OO表示法(比如OMT[8]),基本的的C++编程语言特性(比如类、继承、动态绑定和参数化类型[9])、基本的UNIX系统编程概念(比如进程管理、虚拟内存和进程间通信[10]),以及网络术语(比如客户/服务器体系结构[11]、RPC[12]、CORBA[13]和TCP/IP[14, 15])。
一般而言,理解本论文并不需要对并发有深入的了解;特别地,也不需要对Solaris/POSIX/Win32多线程和同步机制有深入的了解。对并发编程和多线程的综述在4.3介绍,在其中定义了关键的术语,并概述了多种用于在Solaris 2.x、POSIX pthreads和Win32线程上进行并发编程的可选机制。
本论文被组织如下:4.2给出对ACE OS线程封装库的目标的综述,并概述该库的组件的面向对象体系结构。4.3一般性地介绍并发编程的相关背景材料,并特别介绍了Solaris多线程模型。4.4介绍一种激发了ACE线程封装库的设计的最终用户视点,并聚焦于从并发客户/服务器应用中精选的一个使用实例。4.5详细描述ACE线程封装库的公共接口和内部设计。4.6介绍了若干例子,演示在4.5中定义的OO组件。最后,4.7给出结束语。
 
4.2 ACE OO并发机制综述
 
4.2.1总体目标
 
与前几代SunOS相比,现代操作系统(比如Solaris、OSF/1、Windows NT和OS/2)的一种显著特性是其集成的对内核级和用户级多线程及同步的支持。但是,现有的与这些操作系统一起发布的多线程和同步机制都是用C写成的相对低级的API。混合使用C++类和低级C API来开发应用给开发者造成了不可接受的负担。在单个应用中混合这两种风格将导致面向对象和过程编程之间的“阻抗”失配。这样一种混合的编程风格让人迷惑,并会带来慢性的维护问题。
为避免让每个开发者实现他们自己特别的OS线程机制的C++包装,ACE提供了一组在此论文中描述的面向对象的并发组件。这些ACE组件为并发编程提供了可移植和可扩展的接口。该接口简化了用于开发客户和服务器的线程管理和同步机制。它已被移植到POSIX pthreads标准的许多试验版本[3]、Solaris线程[2]、Microsoft Win32线程[4],以及VxWorks tasks。
 
4.2.1.1总体要求
 
与封装和简化OS线程机制的并发底层的目的相结合,ACE OO线程封装类库正在被开发以响应下列常见的应用要求:
 
  • 简化程序设计:通过允许多个应用任务使用传统的同步编程抽象独立地执行(比如CORBA远地方法请求)来实现;
  • 透明地改善性能:通过使用像SPARCcenter 1000和2000共享内存对称多处理器这样的硬件平台的并行处理能力来实现;
  • 显式地改善性能:通过减少数据拷贝,以及通过重叠的通信计算来实现;
  • 改善可感知的响应时间:对于交互式应用(比如用户接口或是网络管理应用),通过将分离的线程与应用中的不同任务或服务相关联来实现。
 
4.2.1.2设计目标
 
ACE OO线程类库被开发用以实现下列设计目标:
 
  • 通过使开发者能够在他们的并发应用中始终一致地使用C++和OO,促进编程风格的一致性。
  • 改善底层并发机制的可移植性和可复用性。
  • 减少为使应用线程安全化所需做出的强制性变动。
  • 排除或减少发生那些微妙的同步错误的潜在可能性。
  • 增强抽象和模块性,而又不牺牲性能。
 
4.2.2 ACE OO线程封装组件的体系化综述
 
图4-1 ACE OO线程封装组件的对象模型
 
图4-1中的Booch对象模型演示了ACE线程封装类库中的组件。这些组件包括下面描述的C++类和类属。
 
4.2.2.1 ACE锁(Lock)类属
 
  • Mutex、Thread_Mutex和Process_Mutex这些类提供简单而高效的机制来序列化对共享资源(比如共享内存中的文件或对象)的访问。它们封装了Solaris、POSIX和Win32同步变量(分别是mutex_t、pthread_mutex_t和HANDLE);在4.5.1.1中描述。
  • RW_Mutex、RM_Thread_Mutex、RW_Process_Mutex这些类序列化对共享资源的访问,其中的内容较少变动,而是更多地用于搜索。它们封装了Solaris rwlock_t同步变量(POSIX pthreads和Win32线程实现使用其他机制);在4.5.1.3中描述。
  • Semaphore、Thread_Semaphore、Process_Semaphore这些类实现了Dijkstra的“计数信号量”抽象(一种用于序列化多线程控制的通用机制)。它们封装了Solaris sema_t同步变量(POSIX pthreads和Win32线程实现使用其他机制);在4.5.1.2中描述。
  • Null_MutexNull_Mutex类提供一种零开销的锁接口实现,被其他C++封装用于同步。此类在4.5.1.5中描述。
  • Token(令牌):Token类提供一种比Mutex更为通用的同步机制。例如,它实现了“递归互斥体”语义,拥有令牌的线程可以重新获取它,而不会导致死锁。此外,当其他线程释放令牌时,阻塞在该令牌上的线程以严格的FIFO(先进先出)的顺序被服务(相反,Mutex并不严格地强制实行一种获取顺序)。该类在4.5.1.6中描述。
  • Recursive_Thread_Mutex通过允许嵌套调用acquire方法(只要拥有该锁的线程也是重新获取它的线程), Recursive_Thread_Mutex扩展了缺省的Solaris线程互斥体语义。它与上面概述的Thread_Mutex类一起工作;在4.5.1.4中描述。
 
4.2.2.2 ACE守卫(Guard)类属
 
  • Guard、Write_Guard和Read_Guard这些类确保在进入和退出一个C++代码块时分别被自动获取和释放锁。它们在4.5.2.1中描述。
  • Thread_ControlThread_Control类与Thread_Manager类相结合,用于在线程的发起函数中自动进行优雅的终止和清扫活动。该类在4.5.2.2中描述。
 
4.2.2.3 ACE条件(Condition)类属
 
  • ConditionCondition类用于在涉及共享数据的条件表达式的状态发生变化时进行阻塞。它封装了Solaris和POSIX pthreads cond_t同步变量(Win32线程使用其他机制实现);在4.5.3.1中描述。
  • Null_ConditionNull_Condition类提供零开销的Condition接口实现,用于单线程应用。它在4.5.3.2中描述。
 
4.2.2.4 ACE管理器(Manager)类属
 
  • Thread_ManagerThread_Manager类含有一套机制来对相互协作以实现集体行为的成组线程进行管理。该类在4.5.4.1中描述。
  • Thread_SpawnThread_Spawn提供一种标准工具,对为并发地处理来自客户的请求所进行的线程创建进行管理。该类在4.5.4.2中描述。
 
4.2.2.5 ACE主动对象(Active Object)类属
 
  • Task(任务):Task类是ACE中用于定义主动对象[16, 17]的中心机制。这些主动对象将输入输出消息排队,并在分离的线程控制中执行用户定义的消息处理服务。该类在4.5.5.1中描述。
 
4.2.2.6杂项ACE并发类
 
  • ThreadThread类封装Solaris线程、POSIX Pthreads和Win32线程族的线程创建、终止和管理例程。该类在4.5.6.1中描述
  • Atomic_OpAtomic_Op类将同步特性透明地参数化进基本的算术操作。该类在4.5.6.2中描述。
  • Barrier(栅栏):Barrier类实现“栅栏同步”,对于许多类型的并行科学应用特别有用。该类在4.5.6.3中描述。
  • TSSTSS类允许“物理上”线程专有的对象被“逻辑地”当作程序的全局对象进行访问。该类在4.5.6.4中描述。
 
4.3并发编程和多线程的背景
 
  大多数UNIX系统程序员都熟悉传统的进程管理系统调用(比如fork、exec、wait和exit)。但是,他们关于正在形成中的UNIX多线程和同步机制(比如Solaris线程[2]、POSIX pthreads[3],或是Win32线程[4])的经验却较少。这一部分将给出对并发编程和Solaris线程的相关背景材料的综述。对并发编程和Solaris/POSIX/Win32线程的更为详细的讨论见[2, 18, 19, 3, 4]。
 
4.3.1进程和线程
 
进程是使程序指令得以执行的一组资源。这些资源包括虚拟内存、I/O描述符、运行时栈、信号处理器、用户和组id,以及访问控制令牌。在早期的UNIX系统上(比如SunOS 4.x),进程是“单线程”的。在UNIX中,单线程程序中的操作通常是同步的,因为控制总是在程序(也就是,用户代码)中,或是在操作系统中(经由系统调用)。在某种程度上,传统UNIX进程的单线程特性简化了编程,因为没有程序员显式地进行干预,进程不会与其他进程相互干扰。
但是,使用单线程进程,有许多应用很难开发(特别是网络服务器)。例如,单线程网络文件服务器不能长期阻塞以处理一个客户请求,因为其他客户的响应性会受到损害。有若干常用方法可用以避免阻塞在单线程服务器中:
 
  • 事件多路分离器/分派器:方法之一是开发一种事件多路分离器/分派器(比如面向对象的反应堆构架[20])。该技术被广泛用于在单线程用户接口构架中管理多个输入设备。主事件多路分离器/分派器检测到来的事件,将其分离到适当的事件处理器,然后分派与该事件处理器相关联的应用特有的回调方法。
该方法的主要缺点是持续时间长的会话必须被开发为有限状态机。当状态的数目增加时该方法将变得相当笨拙。此外,因为只能使用非阻塞的操作,很难通过像“I/O流” 这样的技术、或是数据和指令缓存中的本地引用方案来改善性能。
  • 用户级协同例程(User-level co-routines):另一种方法是开发一个非占先的(non-preemptive)用户级协同例程包,显式地存储和恢复上下文信息。这使得任务能够挂起它们的执行,直到另一个协同例程在后面将它们唤醒。Windows 3.1和Mac System 7 OS上的多任务机制是广泛可用的使用这一方法的系统。
一般而言,要正确地使用协同例程很复杂,因为开发者必须通过周期性地显式派生线程控制来人工地进行任务占先。而且,每个任务必须只执行相对较短的时间。否则,客户将会觉察请求正在被顺序地、而非并发地处理。协同例程的另一局限是,如果OS在任务引发页错误时阻塞进程中所有的任务,应用的性能可能会下降。此外,单个任务(例如,进入了一个无限循环)的失败可能会挂起整个进程。
  • 多进程:降低单线程UNIX进程的复杂性的另一种方法是使用fork和exec系统调用提供的粗粒度多进程能力。fork派生一个分离的子进程,与父进程并发地执行任务。相互分离的进行可以通过使用像共享内存和内存映射文件这样的机制来进行直接的协作。在本地主机上,共享内存是一种比消息传递更为迅捷的IPC方法,因为它避免了显式的数据拷贝。
但是,fork和exec的开销和不灵活使得动态的进程请求对于许多应用来说都极为昂贵和复杂。例如,对于持续时间短的服务(比如解析IP地址的以太网号 、从网络文件服务器获取磁盘块,或是在SNMP MIB中设置属性)来说,进程管理开销就太过度了。而且,使用fork和exec很难对调度和进程优先级进行细粒度的控制。此外,在共享内存段中共享C++对象的进程必须对虚表指针的位置做出不可移植的假定。
 
多线程机制提供了更为优雅,有时也更为高效的方法来克服上述的传统并发进程技术的局限。线程是在进程的上下文中执行的单序列的指令步骤。除了指令指针,线程还包括其他的一些资源,比如函数启用记录的运行时栈、一组通用寄存器,以及线程专有的数据。
传统的工作站操作系统(比如UNIX的一些变种[2, 21, 22]和Windows NT[4])支持多进程(每一个进程包含1或多个线程)的并发执行,每个进程可包含1或多个线程。进程充当被保护的单元、和在单独的硬件保护地址空间中进行资源分配的单元。线程充当在进程地址空间中运行的执行单元,该线程与0或多个线程共享此地址空间。
 
4.3.2基于线程的并发编程的好处
 
因为如下原因,在相互分离的线程、而不是进程中实现执行多任务的并发应用常常是有益的:
 
  • 线程创建:不像生成新进程,派生一个新线程不需要(1)复制父地址空间内存,(2)设置新的内核数据结构,以及(3)消耗一个额外的进程槽,以在大的应用中执行子任务。
  • 上下文切换:线程维护最小限度的状态信息,因而降低了上下文切换的开销,因为只须存储和取回少量状态信息。特别地,线程间的上下文切换消耗的时间比UNIX重量级进程间的上下文切换要少。这是由于在同一进程中的线程间切换时,无需改变TLB虚地址映射。而且,严格地在用户级运行的线程不会带来任何上下文切换开销。
  • 同步:当调度和执行应用的线程时,可能不需要在内核模式和用户模式之间进行切换。进程同步比起线程同步要更为昂贵一些。例如,进行同步的实体常常不是全局的、而是局部的实体。全局同步总是涉及到内核,而应用线程所使用的局部(或“进程内”)同步则可能无需内核的干预。
  • 数据拷贝:分离的线程间通过共享内存进行的通信常常比在分离的进程间使用IPC消息传递要快得多,因为前者避免了显式的数据拷贝的开销。例如,相互协作的数据库服务经常引用驻留内存的公用数据结构,通过线程来实现这些服务可能要更为简单和高效。一般而言,在线程间使用进程的共享地址空间进行通信通常比在进程间使用共享内存机制(比如系统V共享内存或内存映射文件)要更为容易和高效。
 
4.3.3 Solaris上的多进程和多线程综述
 
这一部分总结Solaris 2.x提供的多进程(MP)和多线程(MT)机制的相关背景材料。其他的线程模型和实现(比如SGI、Sequent、OSF/1和Windows NT)的细节有所不同,但基本的概念都是非常类似的。
相对来说,传统的UNIX进程是一种“重量级”的实体,其中含有一个单线程控制。相反,Solaris上可用的基于线程的并发机制要更为成熟、灵活和高效(在适当使用时)。如图4-2所示,Solaris MP/MT体系结构在两个层面上运作(内核空间和用户空间),并含有以下4种组件:
 
图4-2 Solairs 2.x多进程和多线程体系结构
 
  • 处理单元(Processing element:这是执行用户级和内核级指令的CPU。Sun MP/MT模型的语义意图能同时用于单处理器和共享内存的硬件上的对称多处理器。
  • 内核线程(Kernel thread:这是处理单元(PE)在内核空间中调度和执行的基本实体。OS内核为每一内核线程维护一个小数据结构和栈。内核线程间的上下文切换相对较快,因为它不要求改变虚拟内存信息。
  • 轻量级进程(Lightweight process,或LWP:它们与内核线程相关联。在Solaris 2.x中,一个UNIX进程不再是一个线程控制,而是在其中含有一或多个LWP。在LWP和它的内核线程间存在着1对1的映射。Solaris中的内核级调度器使用LWP(因而也包括内核进程)来调度应用任务。LWP含有数量相对较多的状态(比如寄存器数据、账务和特征信息、虚拟内存地址范围和定时器)。因而,LWP间的上下文切换相对较慢。
对于分时调度器类别(缺省),调度器通过“占先”(preemption)将可用的PE在多个活动的LWP间进行划分。通过这种技术,每个LWP运行一段有限的时间(通常为10毫秒)。当前LWP的时间片到期后,OS调度器选择另一个可用的LWP,执行一次上下文切换,并将被占先的LWP放置到一个队列中。内核使用若干标准(比如优先级、资源可用性、调度类别,等等)来调度LWP。在分时调度器类别中,没有固定的LWP执行顺序。
  • 应用线程:每个LWP可被认为是一个“虚拟PE”,在其上应用线程被一个用户级的线程库调度和多路复用。每个应用线程与其他线程一起共享它的进程地址空间,尽管它拥有唯一的栈和寄存器组。应用线程还可以派生出其他应用线程。在进程中,每个这样的应用线程都独立地执行(尽管取决于硬件、并非必然地并发执行)
Solaris 2.x提供一种多层的并发模型,允许使用下面的两种模式来派生和调度应用线程:
 
  1. 绑定线程(Bound thread):它被1对1地映射到LWP和内核线程。绑定线程允许独立任务在多PE上并行执行。因而,如果两个应用线程运行在分离的LWP上(因而也是分离的内核线程上),它们也就可以并行地执行(假定它们运行在多处理器上,或使用异步I/O)。而且,应用线程可以执行阻塞的系统调用并处理页错误,而不会阻止其他应用线程的运行。
重新调度绑定线程需要一次内核级上下文切换。同样地,绑定线程上的同步操作也需要OS内核的干预。当应用被设计利用在硬件平台上可用的并行性优点时,绑定线程最为有用。因为每个绑定线程都要求分配内核资源,分配大量绑定线程可能导致效率低下。
  1. 非绑定线程:它们被一个线程运行时库以多对多的方式在一或多个LWP和内核线程上多路复用。该用户级库实现了一个非占先的协作式多任务并发模型。在使内核干预最少化的同时,它调度、分派,并挂起非绑定线程。与使用绑定到LWP的应用线程相比较,非绑定应用线程的派生、上下文切换和同步所需的开销较小。
取决于应用和/或库与一个进程相关联的内核线程的数目,可以在多PE上并行执行一或多个非绑定线程。因为每个非绑定线程并不分配内核资源,有可能分配数量相当大的非绑定线程,而不会显著地降低性能。
 
4.3.4并发编程的挑战
 
在多处理器上,可以在分离的多个PE上并行地运行多于一个的LWP。在单处理器上,在任何时刻只能有一个活动的LWP。不管是怎样的硬件平台,程序员必须确保对共享资源(比如文件、数据库记录、网络设备、终端,或共享内存)的访问是依次进行的,以防止“竞争状态”(race condition)。竞争状态在两个或多个并发LWP的执行顺序会导致不可预测和错误的结果时发生(比如数据库记录被留在了不一致状态)。竞争状态可使用4.3.5描述的Solaris 2.x同步机制来加以排除。这些机制序列化对共享资源的代码临界区的访问。
除了并发控制的挑战,当使用多线程(而不是多进程,或单线程的反应式事件循环)实现并发应用时还会出现以下的限制:
 
  • 健壮性:在单进程地址空间中通过线程执行所有任务可能会降低应用的健壮性。该问题之所以发生,是因为在同一进程地址空间中的分离线程彼此间并没有受到保护。为了降低上下文切换和同步开销,硬件内存管理单元(MMU)对线程的保护很少,或者根本没有。
因为线程不受保护,进程中一个有缺陷的服务可能破坏它与在进程的其他线程中运行的服务共享的全局数据结构。于是这就可能导致不正确的结果,毁坏整个进程,致使网络服务器无限期地挂起,等等。一个相关的问题是在某个线程中调用的某些UNIX系统调用可能对整个进程产生不希望产生的副作用。例如,exit系统调用具有销毁进程中所有线程的副作用(应使用thr_exit来终止当前线程)。
  • 访问特权:多线程的另一局限是一个进程中的所有线程共享同样的用户id和访问文件和其他受保护资源的特权。因此,为防止对未授权资源意外或故意的访问,那些将安全机制建立在进程所有权之上的网络服务(比如Internet ftp和telnet服务)通常都在分离的进程中实现。
  • 性能:一种常见的误解是认为使应用多线程化将自动提高性能。其实在许多环境中,多线程并不会提高性能。例如,单处理器[19]上专事计算的应用并不会从多线程中获益,因为计算不会涉及通信。另外,细粒度的锁定会导致高昂的同步开销[23, 24]。这也阻止了应用对并行处理的优点进行全面利用。
在有些环境中多线程可以显著地提高性能。例如,通过在多处理器平台上运行,一个多线程的面向连接的应用网关可以从中获益。同样地,在单处理器上,专事I/O的应用也可以从多线程中获益,因为计算涉及到通信和磁盘操作。
 
4.3.5 Solaris 2.x同步和线程机制综述
 
这一部分概述并演示在Solaris 2.x、POSIX pthreads和Win32线程中可用的同步和线程机制。在这些系统中,线程在单进程地址空间中共享若干资源(比如打开的文件、信号处理器,以及全局内存)。因此,它们必须利用同步机制来协调对共享数据的访问,以避免发生4.3.4所讨论的竞争状态。为演示对同步机制的需要,考虑下面的C++代码段:
 
typedef u_long COUNTER;
COUNTER request_count; // At file scope
 
void *run_svc (Queue*q)
{
Message *mb; // Message buffer
 
while (q->dequeue (mb)) > 0)
{
// Keep track of number of requests
++request_count;
 
// Identify request and
// perform service processing here...
}
 
return 0;
}
 
该代码形成了一个网络看守(比如用于医学成像的分布式数据库,或分布式文件服务器)的主事件循环部分。在代码中,主事件循环等待消息从客户到达。当消息到达时,主线程通过dequeue方法将它从消息队列中移除。然后取决于接收到的消息的类型,线程执行某种处理(例如,图像数据库查询、文件更新,等等)。request_count变量追踪到来的客户请求的数量。该信息可用于更新SNMP MIB中的属性。
只要run_svc在单线程控制中执行,上面所示的代码工作良好。但是,当run_svc由运行在不同的PE上的多线程控制同时执行时,在许多多处理器平台上将会产生不正确的结果。这里的问题是这些代码并非是“线程安全”的,因为针对全局变量request_count的自增操作含有一个竞争状态。因而,不同的线程可能会增加存储在它们自己的PE数据缓存中的request_count变量的陈旧版本。
这一现象可通过执行下面的例4-1中的C++代码来演示,运行环境为一台运行Solaris 2.x操作系统的共享内存多处理器。Solaris 2.x允许多线程控制在共享内存多处理器上并行执行。下面所示的例子是上面演示的网络看守的简化版本:
 
例4-1
typedef u_long COUNTER;
static COUNTER request_count; // At file scope
 
void *run_svc (int iterations)
{
for (int i = 0; i < iterations; i++)
++request_count; // Count # of requests
 
return (void *) iterations;
}
 
typedef void *(*THR_FUNC)(void *);
 
// Main driver function for the
// multi-threaded server.
 
int main (int argc, char *argv[])
{
int n_threads = argc > 1 ? atoi (argv[1]) : 4;
int n_iterations = argc > 2 ? atoi (argv[2]) : 1000000;
thread_t t_id;
 
// Divide iterations evenly among threads.
int iterations = n_iterations / n_threads;
 
// Spawn off N threads to run in parallel.
for (int i = 0; i < n_threads; i++)
thr_create (0, 0, THR_FUNC (&run_svc),
(void *) iterations,
THR_BOUND | THR_SUSPENDED,
&t_id);
 
// Resume all suspended threads
// (threads id’s are contiguous...)
for (i = 0; i < n_threads; i++)
thr_continue (t_id--);
 
// Wait for all threads to exit.
int status;
 
while (thr_join (0, &t_id, (void **) &status) == 0)
cout << "thread id = " << t_id
<< ", status = " << status << endl;
 
cout << n_iterations << " = iterations\n"
<< request_count << " = request count"
<< endl;
 
return 0;
}
 
Solaris thr_create线程库例程被调用n_thread次,以派生n个新线程控制。在此例中,每个新创建的线程都传递iterations的值给run_svc函数,作为它唯一的参数,并执行之。该值使得run_svc例程循环n_iterations/n_threads次。
每个线程都使用THR_BOUND和THR_SUSPENDED标志来派生。THR_BOUND通知Solaris线程运行时库将该线程绑定到专用的LWP。每个LWP可以在一个多处理器系统中的单独的PE上并行运行。THR_SUSPENDED标志创建“挂起”状态的线程,确保在调用thr_continue恢复(resume)测试之前,所有线程被完全地初始化。thr_continue函数是一个Solaris线程库例程,可恢复挂起线程的执行。注意此例利用了Solaris是以升序连续分配线程id的事实。
一旦所有线程都已被恢复,thr_join例程就阻塞主线程的执行。thr_join与UNIX wait系统调用相类似棗它获取退出线程的状态。thr_join将“收割”线程,并返回0,直到运行run_svc的线程全部退出为止。当所有其他的线程退出后,主线程打印出iterations的总数,以及request_count的最后值,然后退出程序。
将此代码编译成可执行的a.out文件,并以1 个线程循环10,000,000次的方式运行它,得到如下结果:
 
% a.out 1 10000000
thread id = 4, status = 1000000
10000000 = iterations
10000000 = request count
 
该结果正如所愿。但是,当以4个线程循环10,000,000次的方式在4个PE的机器上运行时,程序打印出:
 
% a.out 4 10000000
thread id = 5, status = 1000000
thread id = 7, status = 1000000
thread id = 6, status = 1000000
thread id = 4, status = 1000000
10000000 = iterations
5000000 = request count
 
显然,有什么出错了,因为全局变量request_count的值只是循环总数的一半。这里的问题是变量request_count的自增没有被正确地序列化。
一般而言,在不提供“强有序缓存一致性模型”(strong sequential order cache consistency model)的共享内存多处理器平台上并行执行时,run_svc会产生不正确的结果。为增强性能,许多共享内存多处理器采用“弱有序缓存一致性语义”(weakly-ordered cache consistency semantics)。例如,SPARC多处理器的V.8和V.9家族同时提供“总存储序”(total store order)和“部分存储序”(partial store order)内存缓存一致性语义。对于总存储序语义,对正在被其他PE上的线程访问的变量的读取和同时进行的对同一变量的写也许不会被序列化。同样地,对于部分存储序语义,写操作与写操作也可能不会被序列化。在任一情形中,由于多PE间的缓存延迟,需要对内存位置进行不止一次装载和存储的表达式(比如foo++或i = i – 10)可能会产生不一致的结果。为确保线程间共享的变量的读写被正确更新,程序员必须人工地保证对这些变量的变动成为全局可见的。
在“总存储序”或“部分存储序”共享内存多处理器上强制实现强有序的一种常用技术是使用同步机制来保护request_count变量的增长。Solaris 2.x提供若干种同步机制。本论文描述Solaris 2.x提供的四种主要同步机制的C++包装:互斥体读者/作者锁计数信号量,以及条件变量[19]。ACE含有封装这四种Solaris 2.x同步机制(分别为mutex_t、rwlock_t、sema_t,以及cond_t)的C++包装。在4.3的余下部分我们将概述Solaris同步机制的行为。4.4演示C++包装的使用,用以简化常见的同步变量的使用,并改善程序的可*性。
 
4.3.5.1互斥锁
 
互斥锁(通常称为“互斥体”或“二元信号量”)用于保护多线程控制并发访问的共享资源的完整性。互斥体通过定义临界区来序列化多线程控制的执行,在临界区中每一时刻只有一个线程在执行它的代码。互斥体简单(例如,只有拥有该互斥体的线程可以释放它)而高效(时间和空间)。
像Solaris 2.x这样的操作系统中的互斥体变量的操作通过可适配回旋锁(spin-locks)来实现。回旋锁通过使用一条原子的硬件指令来确保互斥。对于特定类型的短暂资源争用(像上面的例4-1演示的全局request_count变量的自增),它是简单而高效的同步机制。可适配回旋锁使用原子的硬件指令来轮询指定的内存位置,直到下面的情况之一发生[2]:
 
  • 该位置的值被当前拥有该锁的线程改变。这表示锁已被释放,并可以被回旋的线程获取。
  • 持有该锁的线程进入睡眠。此时,回旋线程也让自己进入睡眠,以避免不必要的轮询。
 
在多处理器上,回旋锁带来的开销相对较小。基于硬件的轮询不会导致系统总线上的争用,因为它仅仅影响在互斥体上回旋的线程的本地PE缓存。
互斥体的一种简单而高效的类型是“非递归”互斥体。非递归互斥体不允许当前拥有互斥体的线程在释放它之前重新获取它。否则,将会立即发生死锁。Solaris 2.x和POSIX pthreads通过mutex_t数据类型和相关的mutex_lock和mutex_unlock函数来实现非递归互斥体。
POSIX pthreads和Win32线程都实现了递归和非递归互斥体(其他类型的互斥体在4.4.4讨论)。如4.5.1.4所描述的,ACE OO线程封装库提供互斥体C++封装,以可移植地实现非递归互斥体语义。非递归互斥体在ACE Recursive_Thread_Mutex类中可移植地实现。
 
4.3.5.2读者/作者锁
 
读者/作者锁与互斥体相类似。例如,获取读者/作者锁的线程也必须释放它。多个线程可同时获取一个读者/作者锁用于读,但只有一个线程可以获取该锁用于写。当互斥体保护的资源用于读远比用于写要频繁时,读者/作者互斥体有助于改善并发的执行。
Solaris 2.x通过它的rwlock_t类型来支持读者/作者互斥体。无论是POSIX pthreads,还是Win32线程都没有本地的读者/作者锁支持。如4.5.1.3所描述的,ACE线程库提供了一个叫作RW_Mutex的类,在C++封装类中可移植地实现了读者/作者锁的语义。读者/作者锁的Solaris和ACE实现都将优先选择权给作者。因而,如果有多个读者和一个作者在锁上等待,作者将会首先获取它。
 
4.3.5.3计数信号量
 
在概念上,计数信号量是可以原子地增减的整数。如果线程试图减少一个值为零的信号量的值,它就会阻塞,直到另一个线程增加该信号量的值。
计数信号量用于追踪共享程序状态的变化。它们记录某种特定事件的发生。因为信号量维护状态,它们允许线程根据该状态来作决定,即使事件是发生在过去。
信号量比互斥体效率要低,因为它们保持额外的状态,并使用休眠锁,而不是回旋锁。但是,它们要更为通用,因为它们无需被最初获取它们的同一线程获取和释放。这使得它们能够用于异步的执行上下文中(比如信号处理器)。
Solaris 2.x通过它的sema_t类型支持信号量。Win32将信号量作为HANDLE来支持。POSIX pthreads没有本地的信号量支持。如4.5.1.2所描述的,ACE线程库提供一个叫作Semaphore的类来可移植地在C++包装类中实现信号量语义。
 
4.3.5.4条件变量
 
条件变量提供风格与互斥体、读者/作者锁和计数信号量不同的锁定机制。当持有锁的线程在临界区执行代码时,这三种机制让协作线程进行等待。相反,条件变量通常被一个线程用于使自己等待,直到一个涉及共享数据的条件表达式到达特定的状态。当另外的协作线程指示共享数据的状态已发生变化,调度器就唤醒一个在该条件变量上挂起的线程。于是新唤醒的线程重新对它的条件表达式进行求值,如果共享数据已到达合适状态,就恢复处理。
被条件变量等待的条件表达式可以任意地复杂。一般而言,与其他同步机制相比,条件变量允许更为复杂的调度决策。条件变量同步使用休眠锁来实现,休眠锁触发上下文切换,并允许另一线程执行,直到锁被获取。如4.3.5.1所描述的,互斥体使用可适配回旋锁来实现。如果线程必须长时间地等待某特定条件实现,回旋锁就会消耗过多的资源。
对于涉及条件表达式语义的情况,条件变量比信号量或互斥体要更为有用。在这种情况下,等待线程必须阻塞到特定的涉及共享状态的条件表达式变为真(例如,表不再为空和网络流控制减轻)。在这种情况下,不需要维护事件历史。因而,条件表达式不记录它们什么时候被“置位”。如果没有正确地使用,这可能会导致“丢失的苏醒”(lost wakeup)问题[19]。
Solaris 2.x和POSIX pthreads通过cond_t类型支持条件变量。本地的Win32 API不支持条件变量。如4.5.3.1所描述的,ACE线程库提供一个叫作Condition的类来可移植地在C++包装类中实现条件变量语义。
 
4.3.6进程vs.线程同步语义
 
为增加灵活性和改善性能,Solaris 2.x提供两种风格的同步语义,并为下列两种情况而优化:(1)在同一进程中执行的线程(也就是,进程内序列化)以及(2)在相互分离的进程中执行的线程(也就是,进程间序列化)。在Solaris 2.x中,同步机制的*_init函数的USYNC_THREAD标志创建为单个进程中的线程而优化的变量。同样地,USYNC_PROCESS标志创建跨进程有效的同步变量。后一种类型的同步机制更为通用,尽管略为低效,如果所有线程是在单一进程中运行的话。
 
4.3.7互斥体例子
 
下面的代码演示怎样将Solaris互斥体变量用于解决我们先前考察的request_count的自增序列化问题:
 
例4-2
 
typedef u_long COUNTER;
// At file scope
static COUNTER request_count;
// mutex protecting request_count (initialized to zero).
static mutex_t m;
 
void *run_svc (void *)
{
for (int i = 0; i < iterations; i++)
{
mutex_lock (&m); // Acquire lock
++request_count; // Count # of requests
mutex_unlock (&m); // Release lock
}
 
return (void *) iterations;
}
 
在上面的代码中,m是mutex_t类型的全局变量。在Solaris中,凡是被置零的同步变量都使用它的缺省语义来初始化。例如,mutex_t变量m总是被初始化成从未锁定状态开始。因此,当mutex_lock第一次被调用时,它将获得该锁的所有权。任何其他想要获取该锁的线程都必须等待(例如,通过“回旋”),直到锁的属主释放m。
上面所示的例4-2解决了原来的同步问题,但它仍具有以下缺陷:
 
  • 不优雅和不一致:代码混合了C函数和C++代码,以及不同的标识符命名习惯。使用混合的编程风格让人困惑,还可能带来维护问题。
  • 强制性:该方案要求改变源代码。在开发大型软件系统时,人工地进行这样的变动会导致维护问题,如果所有变动没有一致地进行的话。
  • 不可移植:该代码只能与Solaris 2.x同步机制一起工作。特别地,移植该代码以使用POSIX pthreads和Windows NT线程将需要改变锁定代码。
  • 易错:程序员很容易忘记调用mutex_unlock。这会使其他正试图获取该锁的线程饿死。而且,如果锁的属主试图重新获取它已经拥有的互斥体,就会发生死锁。
 
程序员还可能忘记初始化互斥体变量。如上面所提到的,在Solaris 2.x中,被置零的mutex_t变量被隐含地初始化。但是,并不能保证在动态分配的结构或类中作为域被分配的mutex_t变量也被隐含地初始化。而且,其他线程机制(比如POSIX pthreads和Windows NT线程)并没有这样的初始化策略,所有同步对象都必须显式地初始化。
 
在4.4我们将检查通过改善Solaris同步机制的功能、可移植性和健壮性,C++包装的使用是怎样帮助克服这些问题的。
 
4.4通过OOC++简化并发编程
 
这一部分检查一个使用实例,以演示在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++方案。
 
4.4.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也还是需要对原来的非线程安全的代码进行强制性的改变。
 
4.4.2另一种C++方案
 
确保锁被自动释放的一种直截了当的方法是使用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.
}
 
遗憾的是,这样的“花括号省略”有副作用:它通过序列化主事件循环、消除了应用中所有并发执行。因此,所有应该在那段代码区中并行执行的计算都会被不必要地序列化。
 
4.4.3改良的C++方案
 
要以一种透明的、非强制的和高效的方式解决现存的问题,需要使用两种另外的C++特性:参数化类型和操作符重载。我们使用这些特性来提供一个称为Atomic_Op的模板类,其部分代码显示如下(完整的接口见4.5.6.2):
 
template
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_OpCOUNTER;
// 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_Opatomic_double;
Atomic_Opatomic_complex;
 
4.4.4通过参数化互斥机制类型扩展Atomic_Op
 
尽管上面描述的Atomic_op和Guard类的设计产生了正确和透明的线程安全程序,还是存在着足资改进的空间。特别地,注意Thread_Mutex数据成员的类型是被硬编码进Atomic_Op类的。既然在C++中可以使用模板,这样的设计决策就是一种不必要的、可以轻易克服的限制。解决方案就是参数化Guard,并增加另一种类型参数给模板类Atomic_Op,如下所示:
 
template
class Guard
{
// Basically the same as before...
private:
// new data member change.
const LOCK &lock_;
};
 
template
class Atomic_Op
{
TYPE operator++ (void)
{
Guardmonitor (lock_);
return ++count_;
}
// ...
 
private:
LOCK lock_; // new data member
TYPE count_;
};
 
使用这个新类,我们可以在源代码的开始处作出下面的简单变动:
 
typedef Atomic_OpCOUNTER;
// At file scope.
COUNTER request_count;
 
// ... same as before
 
4.4.5设计原理和性能问题
 
在作出上面描述的变动之前,很值得分析一下使用模板来参数化程序所用的互斥机制类型的动机是否是有益的。毕竟,仅仅因为C++支持模板,并不意味着它们在所有的环境中就都有用。事实上,通过模板来参数化和归纳问题空间,而又缺少清晰而足够的理由,可能会增加理解和复用C++类的难度。
Atomic_Op类中模板的使用引发了若干问题。第一个是“所有增加的抽象的运行时性能代价是什么?”第二个问题是“除了模板,为什么不使用继承和动态绑定来强调统一的互斥体接口并共享通用的代码?”第三是“程序的同步属性不是因为使用模板和重载而变得不明晰了吗?”若干这样的问题是相关的,本节将讨论涉及不同设计选择的折衷。
 
4.4.5.1 性能
 
为Atomic_Op类选用模板的主要原因涉及运行时效率。在模板实例化的过程中,一旦优化C++编译器将其展开,额外的运行时开销很少,或根本不存在。相反,继承和动态绑定会带来用以分派虚方法调用的运行时开销。
图4-3演示例4-2到例4-5中使用的互斥技术所展现的性能。该图描述处理1千万次迭代所需的秒数,迭代次数被划分为2.5百万次迭代/线程。测试例子使用Sun C++ 3.0.1编译器的-O4优化级编译。每个测试在另外一台闲置的4 PE Sun SPARCserver 690MP上执行10次。结果被平均,以减少虚假的偏差量(此数量被证明是微不足道的)。
 
 
图4-3 处理10,000,000次迭代所需秒数
 
例4-2直接使用Solaris mutex_t函数。例4-3使用C++ Thread_Mutex类包装接口。令人惊讶的是,该实现始终都优于例4-1,而例4-1是直接调用底层Solaris互斥体函数。例4-4在一对花括号块中使用Guard助手类,以确保Thread_Mutex被自动释放。该版本所需的执行时间最多。最后,例4-5使用Atomic_Op模板类,比直接使用Solaris互斥体函数的只稍稍低效一点。更为强势的优化C++编译器可能还会减少这些结果的偏差量。
 
例子
微秒/操作
比率
例4-2
2.76
1
例4-3
2.35
0.85
例4-4
4.24
1.54
例4-5
3.39
1.29
表4-1 不同例子的序列化时间
 
表4-1显示从例4-2到例4-5每次互斥操作所花费的微秒数。注意每次循环迭代需要2次互斥体操作(一次获取锁,一次释放锁)。例4-2被用作基准值,因为它直接使用底层Solaris原语。例4-3到例4-5的第三列通过将它们的值除以例4-2的值来进行规格化。
 
4.4.5.2 可移植性
 
参数化互斥机制类型的一个动机是增强跨平台可移植性。模板使形式的参数类名“Thread_Mutex”与实际用于提供互斥的类的名字去耦合。这对于已经使用符号Thread_Mutex来表示现有类型或功能的平台来说是有用的。在移植到这样的平台时,通过使用模板,不需要对Atomic_Op类源代码进行任何改变。
但是,更为有趣的动机来自于对下述事实的观察:一个人可能实际上想使用若干不同风格的互斥体语义(在同一程序中,或跨越一个相关程序族)。每个这样的互斥风格共享同一基本的获取和释放协议,但它们持有拥有不同的序列化和性能属性。4.5.1.1介绍许多已在实践中被证明为有用的互斥机制。
 
4.4.5.3 透明性
 
一种反对使用模板来参数化同步的意见认为透明度隐藏了程序的互斥语义。这被认为是“臭虫”还是“特性”取决于开发者是否认为并发和同步应被集成进程序中。对于包含基本的“积木”组件(比如4.5.1.1描述的Map_Manager)的类库,常常希望允许同步语义被参数化,因为这使得开发者能够精确地控制和指定他们想要的并发语义。这一策略的替换方案有:(1)如果使用多线程,就不使用类库(这显然限制了功能),(2)在库的外面完成锁定(这可能是低效或不安全的),或者(3)将锁定策略硬编码进库的实现(这也是不灵活和潜在地低效的)。所有这些方案都是违背面向对象软件系统中的复用原则的。
 
4.4.5.4 折衷评估
 
选择适当的设计策略来开发支持并发的类库依赖于若干因素。例如,某些库用户可能会欢迎简单的、看上去隐藏了并发控制机制的接口。相反,其他库用户可能愿意接受更复杂的接口,以获取额外的控制和更高的效率。一种分层的类库设计方法也许可以同时满足这两种用户。使用这样的设计方法,类库的最底层输出大多数或全部的参数化类型作为模板参数。较高层则提供合理的缺省类型值,并提供易于使用的应用开发者编程接口。
最近被ANSI C++委员会采纳的新的“缺省模板参数”特性便利了同时满足两种类型库用户的类库的开发。该特性允许库开发者指定常用的缺省类型作为模板类和函数定义的参数。例如,下面对模板类Atomic_Op的修改为之提供了常用的缺省模板参数:
 
template
class TYPE = u_long>
class Atomic_Op
{
41/41234>
 

评分:0

我来说两句

seccode