AC:本地语言的可组合异步IO论文翻译

AC:本地语言的可组合异步IO

摘要:

本文介绍了AC,一种在C/ c++等本地语言中用于可组合异步IO的语言构造。与传统的同步IO接口不同,AC允许线程发出多个IO请求,以便并发地对它们进行服务,因此长延迟操作可以与计算重叠。与传统的异步IO接口不同,AC保留了一种顺序的编程风格,不需要代码使用多个线程,也不需要代码被“栈分解”成回调链。AC提供了一个async语句来确定IO操作并发发出的机会,一个do…finish块来等待任何封装的异步工作完成,以及一个cancel语句来请求取消封装的do…finish中未完成的IO。我们给出了核心语言的操作语义。我们描述和评估了集成在Barrelfish研究操作系统上的消息传递和集成在Microsoft Windows上的异步文件和网络IO的实现。我们展示了AC为异步IO提供了与现有C/ c++接口相当的性能,同时提供了更简单的编程模型。

介绍:

在未来,处理器很可能在整个机器上提供不同类型的核心,而不需要硬件缓存一致性。在Barrelfish项目中,我们正在研究如何为这类硬件设计一个操作系统(OS),在这个操作系统中,我们不再依赖操作系统[4]中的传统共享内存。

我们采用的方法是围绕独立的每核内核构建操作系统,并使用消息传递在运行在不同内核上的系统进程之间进行通信。其他当代操作系统研究项目采用了类似的方法[38]。我们的假设是,建立在消息传递之上的系统可以映射到各种各样的处理器体系结构,而无需大规模的重新实现。使用消息传递让我们能够适应具有异构核心类型的机器,以及没有缓存一致性的机器;我们可以将消息传递操作映射到专门的消息传递指令[18,34],也可以将它们映射到当前硬件[4]上的共享内存缓冲区。

然而,利用消息传递来编写可伸缩的底层软件是一件困难的事情。现有系统要么关注编程的便利性(通过提供简单的同步发送/接收操作),要么关注性能(通常通过提供异步操作,在发送或接收消息后执行回调函数)。同样的张力更普遍地存在于IO接口中[27,35]。例如,Microsoft Windows api要求软件在同步操作(每个线程只允许一个并发IO请求)和复杂的异步操作(允许多个IO请求)之间进行选择。

我们相信,硬件不可避免地向非缓存一致、异构、多核系统演化,这使得在C/ c++等低级语言中支持异步IO既必要又及时。

本文介绍了一种利用异步IO (AIO)编写程序的新方法AC(“异步C”)。AC提供了一种轻量级的AIO形式,可以将其增量地添加到软件中,而不需要使用回调、事件或多线程。

我们的总体方法是让程序员从简单的同步IO操作开始,并使用新的语言构造来确定语言运行时系统异步启动多个IO操作的机会。

作为一个正在运行的示例,考虑一个Lookup函数,该函数向名称服务进程发送消息,然后接收该名称映射到的地址。图1显示了使用Barrelfish的基于回调的接口编写的这个函数。

Lookup函数接受对一个通道©的引用。该函数注册一个ResponseHandler回调函数,以便在接收到一个LookupResponse应答时执行。然后它注册一个SendHandler回调,以便在通道c有空间发送消息时执行。(许多消息传递的硬件实现提供了大小有限的消息通道,因此不可能立即发送消息。)此外,Lookup需要在临时数据结构中记录名称,以便SendHandler可以使用它。On*函数是从NSChannel_t通道的接口定义中自动生成的。

对于AC,“查找”示例变成了一个使用同步Send/Recv操作的单个函数:(我们省略了一些关于取消未完成的IO操作的细节;我们回到第二节的取消。)

与基于回调的实现相比,这个LookupAC函数显然要简单得多:它避免了“栈转换”[3]的需要,在[3]中,操作之间的逻辑流在一系列回调之间被分割。AC导致了一种形式的可组合性,而这种可组合性在叠取过程中丧失了。一个函数可以简单地使用AC调用其他函数,并且可以同时启动多个AC操作。例如,要与两个名称服务器通信,可以这样写:

async at语句S1表示,如果第一次查找需要阻塞,则可以继续执行语句S2。finish构造表明,在S1和S2执行完成之前,不能继续执行S3语句。

在整个AC中,我们将用于异步的抽象与用于并行编程的抽象分开;除非程序员显式地引入并行性,否则代码仍然是单线程的。async和do…finish构造仅用于确定并发发布多个消息的机会;与X10[7]中的异步构造不同,我们的异步没有引入并行性。因此,除了do…finish的块结构同步之外,我们的许多示例都可以在没有并发控制的情况下编写。

除了AC的核心设计之外,我们还做了一些额外的贡献。我们引入了一个新的块结构的取消机制。这种取消的方法为程序提供了一种模块化的方式来启动异步操作,然后在它们尚未完成时取消它们;例如,在被调用的函数周围添加一个超时。在TwinLookupAC中,取消可以用来在另一个查找完成后立即放弃一个查找。与我们的方法相反,传统的取消机制针对单个IO操作[1],或针对同一文件上的一组操作,或针对一个完整的线程(例如,Modula-2+[5]中的警报)。

我们将在第2节详细介绍交流电。在第3节中,我们提出了一个核心语言建模AC的形式化操作语义,包括取消。我们给出语义并讨论它所满足的属性。

第4节描述了AC的两种实现。第一个实现使用了修改过的Clang/LLVM工具链来将AC操作添加到C/ c++中。第二个实现使用Clang或GCC操作,并使用宏和这些编译器提供的现有C/ c++扩展的组合定义AC构造。第二个实现的开销略高于第一个。

在第5节中,我们将讨论与Barrelfish上的消息传递集成的实现以及与Microsoft Windows上的异步IO集成的实现的性能。在每种情况下,AC都实现了手工编写的栈剥离代码的大部分性能,同时提供了一个可与基本同步IO相媲美的编程模型(也可与最近用于执行异步IO[32]的基于c#和f#的抽象相媲美)。我们将在第6节和第7节讨论相关工作并进行总结。

2.可组合异步

在这一节中,我们将非正式地介绍空调。我们继续介绍中的名称服务查找示例。我们使用它来更详细地说明AC操作的行为,并激发我们的设计选择。

在本节中,我们的设计选择是由两个属性驱动的。首先,一个“串行省略”属性:如果在一个软件中IO操作完成而不需要阻塞,那么软件的行为就像AC扩展被删除一样。第二,一个“同步省略”属性:删除AC构造后,会留下一个使用普通同步操作的正确程序。相反,使用同步程序并添加这些构造将生成使用异步IO的程序。我们相信,这两个属性在简化现有应用程序使用异步的增量调整方面是有价值的(当然,仍然需要注意准确地确定哪些IO请求可以同时发出)。

在本节中,我们将重点关注基于Barrelfish上的消息传递的示例。在此设置中,AC发送/接收操作将阻塞,直到传出消息已在通道中缓冲,或直到传入消息已从通道中移除。与CSP[17]等语言中的消息传递操作不同,AC发送/接收操作彼此之间不同步。通道在一台机器的一对对进程之间进行操作。它们提供可靠的、有序的消息传递。唯一的一种故障是当一端的进程没有关闭通道而终止时,通道突然断开;这个失败被通知给其他进程,因此错误处理代码在这里的示例中没有出现。

从介绍中可以很容易地编写LookupAC这样的函数:同步消息传递避免了基于回调的接口的复杂性。然而,它也失去了好处:我们不能再同时执行多个发送/接收操作,我们不能再在等待IO操作完成时进行表单计算,我们不能在IO操作启动后放弃等待。

AC通过提供async和do…finish构造来解决这个问题,以允许多个IO操作被发出(章节2.1),并提供cancel构造用于块结构的等待取消(章节2.2)。

2.1 async和do…finish构造

async和do…finish构造提供了一种机制,可以在某个操作阻塞时切换掉该操作,并在该操作解除阻塞后恢复执行。图2给出了一个示例,扩展了早期的LookupAC函数,以查询一系列服务器,并在结果不同时报告错误。LookupOneAC函数执行一次查找,返回结果地址。LookupAllAC函数接受一个通道数组,并对LookupOneAC进行一系列调用以执行每次查找。循环中的async表示如果给定的LookupOneAC调用阻塞,则执行可以继续到下一次迭代,而do…finish表示执行必须在完成时阻塞,直到所有异步工作完成。这个示例满足同步省略属性:如果忽略了新构造,那么它就变成了一个简单的、依次在每个服务器上进行的顺序查找。

这里有许多设计选择:

异步开始工作。首先,当到达一个语句时运行什么代码。, S本身,或async S语句的延续(如AME[19]),或它们是交错的(如X10 [7])?在AC中,执行立即进入S,而不引入并行执行。这个特性既遵循了串行省略属性,也遵循了我们在概念上希望将对异步和并行的支持分开的愿望。

这个设计选择的结果是,在图2中,异步声明内的代码可以简单地读我获取渠道:内部异步的代码语句直接运行在每个循环迭代的开始在我修改下一个执行循环的头。

该示例还利用了异步不引入并行的事实:当LookupOneAC返回时,不需要同步访问本地变量result、first result或seen first。例如,我们不需要对这些变量引入锁,也不需要使用future将LookupOneAC的值传递给它的调用者。

如果一个局部变量在一个异步语句中声明,那么它对该语句的每次调用都是私有的(不像图2中的例子,在异步语句的调用之间共享变量)。

在异步工作中阻塞。下一个设计选择是当代码在异步S块中时会发生什么。在AC中,当S第一个块时,继续执行异步语句。在这方面,async可以被看作是“捕获”它内部代码的阻塞,并为周围的代码提供一个机会来启动额外的IO操作,或者进行计算。在图2中,async语句的延续是循环头,它将继续进行下一个迭代。

当调用一个可能阻塞的函数时,程序员需要预测在被调用者返回之前可能运行其他代码的可能性。我们遵循一个惯例,所有可能阻塞的函数的名称上都有一个AC后缀。对于原始的发送/接收操作,以及LookupAllAC这样的示例,这种约定是正确的。遵循此约定可以确保调用者知道在等待时执行可能会切换到线程的其他位置。例如,在图2中,局部变量i的值可能会在调用LookupOneAC时被更新,因此如果需要原始值,那么应该在调用之前保存它。

突出显示AC操作的约定与“默认原子”编程模型(如AME[2,19]和TIC[30])中的规则相对应,即非原子的操作应该在函数定义和每个调用点上包含注释。如果需要的话,可以通过静态检查来加强我们的约定。一种简单、保守的方法是,如果从非AC函数调用AC函数,则发出警告。

同步。最后一个设计选择是如何与异步操作同步。do. finish结构提供了这种形式的同步:执行不会超过do…finish,直到它内部开始的所有异步工作都完成。在图2中,do…finish要求所有LookupOneAC调用在LookupAllAC返回之前完成。从LookupAllAC的调用者的角度来看,在do…finish结束时阻塞和在IO操作时阻塞是一样的:调用可以放在async中,如果调用阻塞(例如,不同的LookupAllAC以其他名称阻塞),则可以启动其他工作。

启动异步工作的规则。
异步语句必须在do…finish中静态出现。写不平衡的代码是不正确的,例如:

这个设计选择遵循了我们对C/ c++系统软件实现的关注。与预测的寿命数据用于同步:(i)仙人掌——堆栈可以使用[16],而不需要一个更一般的基于堆的结构,和(2)像往常一样,被可以安全地访问进行数据传递给它,是否任何的无关地调用堆栈是异步的。(CILK也有类似的规则,函数隐式地与它生成[13]的任何并行工作进行同步。)

与线程集成:尽管async没有引入并行性,但我们的实现还是与OS线程集成在一起的。这种集成支持以下场景:多线程服务器在不同线程中处理到不同客户端的连接,或者函数显式启动线程,在函数返回后执行工作(如上面StartAsync的变体)。

运行时系统提供并发控制原语,如互斥和条件变量。这些原语可以在同一操作系统线程中的异步工作片段之间使用,也可以在不同操作系统线程之间使用。并发控制原语上的阻塞处理方式与IO上的阻塞处理方式完全相同:OS线程的执行可以切换到不同的工作。这个工作可以是一个封闭的异步语句的延续,也可以是一个未阻塞的工作。Work保留了对启动它的OS线程的亲和性。原始的消息发送/接收操作本身是线程安全的。

2.2 取消

async和do…finish构造让程序员启动多个IO操作,它们让程序员将计算和通信重叠起来。然而,这些构造并不能恢复底层基于回调的api的所有表达能力;特别是,我们还希望能够在IO操作启动后停止等待。

AC提供了一个取消命令,允许程序停止等待IO操作。取消在某种程度上类似于Java中的线程中断:它导致IO上阻塞的操作被解除阻塞。

图3显示了如何使用取消写LookupFirstAC函数查询一组名称服务器返回的第一反应,然后取消其余查询:内循环做. .完成标记“查询”,查询和取消命令执行时的第一反应。取消会导致do…finish中任何被阻塞的LookupOneAC调用被解除阻塞,然后返回CANCELLED。finish块的行为和往常一样:一旦在它里面开始的所有异步工作都完成了,执行就可以继续到LookupFirstAC的末尾。

当与标签一起使用时,cancel必须在标签标识的do…finish块中静态发生。如果不带标签使用cancel,则它指向最近的静态封闭的do…finish块。如果没有静态封闭的do…finish块,则取消是错误的。这个要求明确了正在取消的操作块:一个人不能调用一个函数,然后通过在内部使用取消而让它意外地“毒害”调用者的函数。

语义的取消。当取消涉及有副作用的操作时,需要小心。即使对于像LookupOneAC这样的“只读”操作,也存在取消后通道状态的问题。例如,如果取消发生在消息发送之后,但在接收到响应之前,会发生什么?如果响应随后到达会发生什么——它会与对不同消息的响应相混淆吗?

在Barrelfish和Microsoft Windows上,我们都遵循一个约定,我们称之为“精确取消”:一旦取消,要么(i)调用返回CANCELLED,而没有执行请求的操作,要么(ii)操作执行后函数返回OK。特别是,如果一个IO操作由一个设备同时完成,而软件正在请求取消,那么即使在执行cancel之后,仍然可以看到OK结果。

这个约定代表了应用程序程序员和IO库实现者之间的工作分工:IO库保证在它自己的函数中提供精确的取消,但应用程序程序员负责在他们编写的函数中提供精确的取消。程序员必须承担这个责任——因为正确的行为依赖于他们所编写的操作的语义。,是否必须执行补偿操作,如果必须执行,具体是什么。

为了允许在不被取消的情况下编写补偿代码,AC让函数被标记为“不可取消”,并且在Barrelfish上,我们提供了所有消息传递原语的不可取消变体。在第4节中,我们将展示这些不可取消原语是如何在AC运行时系统上实现的。

可取消。考虑图4中的示例,该示例向LookupFirstAC调用添加一个超时。第一个异步启动LookupFirstAC请求。第二个异步启动一个计时器。无论哪个操作第一次完成,都会尝试取消另一个操作。这种块结构的方法允许程序以可组合的方式使用取消:在LookupWithTimeoutAC中触发的取消会传播到两个异步分支(并递归地传播到它们的调用,除非这些调用是不可取消的)。

与针对单个操作的取消请求的系统不同,AC允许调用者取消一组操作,而不能单独命名它们。

注意,在LookupWithTimeoutAC函数中,返回值总是取自LookupFirstAC。在检查返回值是否正确时,需要考虑三种情况。首先,如果LookupFirstAC返回OK,那么确切的取消语义意味着已经执行了查找,并且结果可以像往常一样传回来。第二,如果SleepAC超时过期并取消了LookupFirstAC,那么得到的CANCELLED返回值是正确的。最后,如果LookupWithTimeoutAC本身被其调用者取消,那么LookupFirstAC调用的结果将决定LookupWithTimeoutAC操作的总体结果。

4.实现

在本节中,我们将讨论AC的实现。我们将讨论如何通过修改后的编译器(第4.1节)或C/ c++宏(第4.2节)来构建核心语言特性。最后,我们将展示如何将基于回调的异步IO与AC集成(第4.3节)。

4.1 Clang/LLVM Implementation

我们的第一个实现基于Clang/LLVM工具链(v2.7)。我们将异步代码块转换为对编译器生成函数的异步调用。我们通过将async语句的内容转换为LLVM代码块(LLVM中添加的一种闭包形式,作为C的扩展)来实现这个反糖化。

在运行时,do…finish和async的实现基于cactus栈,栈的分支表示已经开始但尚未完成的异步操作。在我们的工作负载中,许多异步调用在没有阻塞的情况下完成(例如,因为在执行接收操作时,一个通道已经包含了一条消息)。因此,我们将尽可能多的簿记工作推迟到异步调用真正阻塞时(就像延迟任务创建[26]一样,在[26]中,线程的创建被推迟到空闲处理器可用时)。特别是,当进行异步调用时,我们不更新任何运行时系统数据结构,并且我们允许被调用方与调用方在同一个堆栈上执行(而不需要堆栈切换)。为了说明这种技术,考虑以下示例:

图6(a)显示了do…finish块中的运行时系统的初始状态。与每个线程相关联的是一个“原子工作项”(AWIs)的运行队列,它表示当线程变为空闲时准备运行的工作片段。具体地说,AWI只是一个保存的程序计数器、堆栈指针和创建AWI的线程的线程ID。为了减少锁定开销,运行队列被结构为一对双链表,一个由线程本身访问而不受并发控制,另一个由其他线程访问(例如,当AWI增加一个信号量时,然后运行AWI的线程可能需要访问在信号量上被阻塞的AWI的运行队列)。

除了运行队列之外,每个线程都有一个“current FB”,用来标识它所在的do…finish块。每个FB (Finish Block)结构在输入do… Finish Block的框架中堆栈分配;finish的语义意味着这个函数只能在块完成时返回。FB有一个cancelled/active标志(最初是ACTV),一个指向动态封装FB的FB结构的指针,一个已经启动但尚未完成的异步调用数量的计数,一个指向特殊“completion AWI”(我们将在下面描述)的引用,和一个双链表持有(i)函数,如果FB被取消,执行,和(ii) FBs,任何do…finish块动态嵌套在这个。在图中,列表是空的。

在本例中,执行从对as1的异步调用开始(图6(b))。按照通常的方式分配一个新的堆栈帧;如果as1正常返回,则执行将在异步调用后继续执行。

然而,如果as1阻塞(图6©),那么运行时系统(i)增加外围FB中阻塞项的计数,(ii)分配一个新的AWI来表示阻塞的工作,将这个AWI放置在as1的堆栈帧的末尾,(iii)遍历堆栈,找到与未恢复continuation的异步调用对应的最近的封闭调用站点(如果有的话)。编译器生成的返回地址表用于标识异步调用。

如果有一个异步调用,那么被调用方的堆栈帧的返回地址将被重写,以进入一个存根函数(如下所述),并为调用方分配一个新的堆栈。在我们的实现中,我们为每个堆栈保留1MB的虚拟地址空间,在这个空间中,我们使用保护页惰性地分配4KB的物理内存页。在图中,堆栈2被分配,并在新堆栈的main中继续执行。为了允许执行在栈之间移动,异步调用使用了一个新的调用约定:(i)在异步调用站点上所有寄存器都被视为调用者保存,(ii)一个帧指针用于所有包含异步调用的函数。第一个规则允许执行恢复后叫无需恢复callee-save寄存器的值,第二个规则允许调用者在不连接的执行堆栈恢复从原始调用(例如,在图6中©)通过恢复原始帧指针但使用新的堆栈指针。

如果函数在没有异步调用时阻塞,则通过从当前线程的运行队列中恢复AWI来继续执行。如果运行队列本身为空,则执行块用于完成异步IO操作。

在本例中,执行在main中继续,并到达do…finish的末尾(图6(d))。此时,运行时系统检查(i) FB是否有任何尚未完成的异步调用(即,count̸=0),(ii)如果计数为0,是否在进入do…finish的原始堆栈上执行。在本例中,当前FB上的计数是非零的,因此执行块。

图6(e)显示了在as1中执行的IO完成时的情况:挂起的AWI被添加到运行队列中,并由线程恢复。然后函数as1完成,并返回到图6©中链接到堆栈的“stub”函数。stub函数重新检查count字段,以检查as1是否是该FB的最后一个未完成的异步函数:在这种情况下,计数被减为0,并在Stack 1的do…finish之外继续执行(图6(f))。

本例中没有使用AWI油田的完井作业。它的作用是确保执行留下. .按时完成块相同的堆栈,进入堆栈(在本例中1)。如果原始栈上运行的工作完成时当前FB的计算仍然是零然后完成AWI初始化工作完成后。然后,当另一个堆栈上的计数达到零时,执行被转移到完成AWI,因此返回到原始堆栈。

4.2 Macro-Based Implementation

除了基于编译器的实现之外,我们还开发了一个基于C/ c++宏的实现,利用现有的扩展来定义嵌套函数。这个实现允许我们在LLVM不支持的平台上使用AC(例如,基于Beehive fpga的处理器[34])。比较这两种实现还可以让我们评估包含语言特性来支持异步IO的优缺点。

与Clang/LLVM实现有两个不同之处:首先,在语法上,基于宏的实现使用ASYNC(X)来表达一个包含语句X的ASYNC语句,DO FINISH(X)用于包含X的DO … FINISH, DO FINISH(lbl,X)用于包含标签lbl的块。DO FINISH宏定义了以调用AC运行时系统开始和结束的块。ASYNC宏定义了一个嵌套函数,该函数包含ASYNC语句的内容,然后它调用AC运行时系统来执行嵌套函数。

第二个区别是,基于宏的实现不生成编译器生成的表,以便在阻塞时遍历堆栈以识别异步调用。因此,我们研究了两种异步调用的替代方法:(i)在进行异步调用时主动分配堆栈;或者(ii)在异步调用期间将显式标记推到堆栈上,只在异步调用阻塞时启用堆栈的惰性分配。紧急分配相对容易实现,但是,正如我们在第5节中所示,它会产生大约110个周期的性能成本

每个异步调用。基于宏的惰性分配允许堆栈分配本身变得惰性,但仍然需要一些迫切的簿记来为调用的延续初始化一个AWI,并且它在每个异步调用上需要额外的间接级别。与编译器集成的惰性实现相比,它为每个异步调用增加了大约30个周期的开销。

4.3 Integrating Callback-Based IO with AC

AC运行时系统提供了一组函数,通过这些函数,异步IO操作与新的语言结构进行交互;这些提供了一种方法,可以将现有的基于回调的抽象调整到可以从AC调用的表单中。

调度。第一组操作用于控制AWIs的调度。挂起(xp)结束当前的AWI,并初始化xp用一个指针指向一个新的AWI,以继续挂起调用。实际上,AWI是在Suspend调用的堆栈框架中分配的,如图6©所示。SuspendUnlock和Suspend是一样的,除了一个给定的自旋锁在阻塞后被释放(我们下面演示它的用法)。Schedule(x)将指向x的AWI添加到AWI所属线程的运行队列中。Yield结束当前的AWI,并立即将Yield的延续添加到当前线程的运行队列中。最后,YieldTo(x)是一个直接yield:如果x属于当前线程,则YieldTo的继续被放到运行队列中,并立即执行x所指的AWI。如果x是一个不同线程的long,则YieldTo(x)等价于Schedule(x)。

取消。图7中的第二组操作与取消交互。这个基本抽象是一个“取消项目”,它是一个回调函数,当取消被触发时运行。这些回调在可取消操作启动时注册,在可取消操作完成时注销。取消项存储在FB结构中包含的do…finish块的双链表中。这些回调在执行取消语句时运行。

例子。我们已经为Barrelfish上的消息传递和Microsoft Windows上的异步IO开发了多个版本的AC。为了简洁起见,我们主要关注在Barrelfish上接收消息,展示同步AC操作是如何在介绍中描述的基于回调的接口上构建的。其他函数遵循类似的模式。

图8显示了总体方法。对于每个消息通道,我们记录一个缓冲消息和一个状态。这些是由运行作为回调的“下半部分”函数和运行作为阻塞AC操作的“上半部分”函数更新的。回调会一直等到缓冲区为空,然后存储下一条消息。上半部分函数会一直等待,直到缓冲区中有消息可用为止。

图9显示了实现的简化版本(逻辑遵循完整版本,但我们使用较短的名称,并省略了一些类型转换和函数参数)。LookupResponse t结构提供缓冲。自旋锁保护对其他字段的访问。state字段记录缓存的数据是否在等待下半部函数(BHWAITING),上半部分函数是否阻塞等待数据(THWAITING),或上半部分函数是否刚刚被取消(TH cancelled)。两个锁序列化上半部分和下半部分处理程序的执行,只允许每种类型的一个在给定的时间在给定的缓冲区上执行。当上半部分函数等待时,rx awi字段存储保存的上下文。addr字段携带缓冲的消息有效负载(在本例中是名称-服务查找返回的地址)。

LookupResponseBH是示例的下半部函数。它等待下半部锁,然后更新缓冲状态。它使用一个直接的YieldTo将执行直接转移到上半部分,如果有一个正在等待。

lookuppresponseac是示例的上半部分函数。它等待上半锁,如果有缓冲消息,则使用它,否则在挂起自己之前将状态标记为TH_WAITING。如果取消发生在上半部分等待时,则执行CancelRecv函数。此功能测试是否已传递消息。如果没有消息被传递,那么CancelRecv将状态更新为TH_CANCELLED,然后恢复上半部分的代码(注意自旋锁从CancelRecv内部保持到上半部分函数的末尾——确保在下一个消息被传递之前状态被重置为EMPTY)。

5.性能评估

在本节中,我们将评估AC的性能。我们首先看看微基准测试,以显示单个AC操作的开销(第5.1节)。然后我们在Barrelfish和Microsoft Windows上使用更大的例子来衡量AC的性能。在Barrelfish上,我们使用AC来实现一个低级能力管理系统(第5.2节)。在Microsoft Windows上,我们在一系列io密集型应用程序中使用AC(章节5.3)。

我们使用一台带有4个四核处理器的AMD64机器(第5.1和5.2节),以及一台带有Intel Core 2 Duo处理器的HP工作站(第5.3节)。所有结果使用优化代码(-O2)。我们验证了修改后的编译器的性能与基线编译器和gcc 4.3.4一致。实验通常有10 000次迭代,使用前9 000次作为热身,并报告最后1 000次的结果。在每一种情况下,我们都确认这种延迟避免了启动的影响。我们报告中值,并给出5-95%的范围内的任何结果具有显著的方差。

5.1 Microbenchmarks

我们比较了(i)普通函数调用、(ii)对空函数的异步调用和(iii)对产生如下结果的函数的异步调用的性能。,阻塞,然后立即解除阻塞。我们研究了基于Clang/ llvm的实现,它使用了惰性簿记,以及两个基于宏的实现。该测试在一个紧密的循环中调用,我们测量了每个调用所需的周期:

这种低级计时不可避免地受到处理器实现和编译细节的影响:然而,结果证实,编译器集成的惰性簿记允许在被调用方不阻塞时使用async,并且执行惰性簿记不会损害性能,如果被调用方随后阻塞。懒惰的、基于宏的实现的性能略低于编译器集成的实现。这个结果反映了基于宏的实现在每个异步调用上引入的额外的间接级别。

我们测量了不同内核上的进程间乒乓微基准测试的性能。我们使用了AC编译器集成的实现。结果显示了从一个核发送消息到相应的响应在同一核上收到的时间:

我们比较了五种实现。“基于回调的API”使用现有的Barrelfish基于回调的消息传递库。这个库提供了一个基于回调的API,它构建在传统系统的共享内存上,并通过硬件消息在系统上传递,如Intel SCC [20],

交流测试的结果显示了在乒乓测试中使用交流的一方或双方的成本。与基于回调的API相比,差异小于15%。跟踪代码,在基于回调和AC变体中执行的操作序列本质上是相同的:使用YieldTo将下半部分回调函数直接绑定到正在等待的上半部分AC接收操作。用非定向进度表替换YieldTo将导致1437个周期的总成本。为了进行比较,我们还在相同的硬件上使用Microsoft HPC-Pack 2008进行了类似的共享内存MPI乒乓测试,发现它的价格是AC的两倍多。

5.2 权能管理

我们的第二个测试在Barrelfish中使用AC。操作系统通过能力来控制对物理资源的访问。拥有一个核心上的功能,就可以在底层资源上执行一组操作,而无需与其他核心同步。操作系统节点采用两阶段提交协议,确保所有核心的能力管理操作顺序一致。这个协议是AC的理想使用场景:它是操作系统的性能关键部分,易于理解和维护的代码是处理复杂性的理想选择。

图10显示了执行功能操作的时间。该协议包括一个初始化核心向所有其他核心发送消息,等待响应,然后将结果发送回其他核心。第一组结果(图10(a))显示了一个人工配置,其中流程在等待传入消息时不受约束地旋转。这种配置让我们可以关注不同实现的最佳性能(在实践中,当没有传入消息时,操作系统会抢占进程)。

在图10(b)中,我们将系统配置为在等待传入消息时抢占进程。“Seq”的性能更差,因为当消息到达时,接收端通常不会运行(注意日志规模)。“事件”、“异步”和“批处理”的实现是不可区分的:使用AC的实现提供了与基于回调的实现相同的性能,同时避免了手动栈剥离的需要。

5.3 IO on Microsoft Windows

我们将AC与微软Windows上现有的异步IO设施集成在一起。

磁盘负载。我们的第一个测试程序是一个合成磁盘工作负载,它对软件RAID系统的行为进行建模。可以将测试配置为直接使用Windows异步IO,或使用AC,或使用基本同步IO。该测试对两个配置为RAID0的Intel 25-M固态存储设备(ssd)进行随机读取,每个设备的持续读取吞吐量最高可达250MB/s(我们运行了其他工作负载,但为了简便起见省略了它们,因为它们在不同实现的性能方面显示了类似的趋势)。AC版本使用ReadFileAC,跟踪已经发出的请求的数量,并在达到限制时阻塞。跟踪IOs让我们可以控制暴露给磁盘子系统的并发请求管道的深度。AIO实现直接使用基于回调的IO接口;与介绍中的示例一样,AIO实现需要手动进行栈分解。

图11分别显示了4k和64k IO块大小的结果。AC和AIO实现实现了类似的吞吐量。使用同步IO的实现要慢得多。

我们比较了AC和AIO实现的CPU消耗。这个实验形成了一个“健全检查”,即AC的吞吐量不会以巨大的CPU消耗为代价。对于4k传输,开销从5.4%到11.8%,而对于64k传输,我们测量到AC使用的CPU周期(用户模式+内核模式)增加了9.8%。随着管道深度的增加,通过AC调度器循环的次数减少

因为每个pass处理多个已完成的请求。这个缓解因素意味着,对于64k读取和1024条请求管道,我们实际上观察到AC的CPU成本更低。

股票报价服务器。我们最后一个测试程序是“股票报价服务器”程序,它用来说明。net框架最新版本的异步编程[32]的特性。服务器每秒向数量可变的客户端发送一条消息。我们在同一台机器上的不同内核上运行服务器和客户机。我们不断增加连接数,直到达到系统限制。我们比较了四种单核服务器实现的性能:(i)一个AC实现,(ii)一个使用异步IO的C实现,(iii)一个使用异步IO的c#实现,(iv)一个使用异步IO的f#实现。AC、c#和f#的实现都避免了栈抓取;C实现被写为响应IO完成事件的堆栈撕裂回调。

图12(a)显示了不同实现的吞吐量,测量了随着客户端连接数量的增加,每秒实际服务的客户端数量。AC实现和堆栈剥离AIO实现一样可以扩展:对于运行在单核上的服务器,两者都可以扩展到大约18000个客户机(在这一点上达到了网络连接的系统限制)。相反,f#和c#客户端分别在10 000和8 000个客户端左右达到饱和。

图12(b)显示了不同实现的CPU消耗,测量服务器进程每秒钟消耗的CPU时间的毫秒数。AC和AIO实现的CPU消耗类似,在10,000个客户端上,不到c#和f#实现的1/5。

我们比较了这些单核服务器实现与使用多个协作线程和同步IO的服务器的性能。多线程服务器的饱和速度是每个核4500个客户端,远远低于AC和AIO实现处理的18000个客户端。这种性能上的差异是由于同步IO操作比异步操作执行更多的内核模式工作。

我们研究了f#和c#实现与AC和AIO实现在性能上不同的原因。所有这些实现最终都构建在操作系统内核的相同异步IO接口上,不同的性能来自实现的用户模式部分。有两个主要因素的作用大致相当。首先,f#和c#实现使用堆分配的临时数据结构来记录关于挂起IO操作的信息。分配这些会增加GC的压力。其次,f#和c#实现严重依赖于“固定”内存中的数据缓冲区,这样缓冲区就不会被GC移动。固定引入了额外的分配和解分配用于固定对象的GCHandle结构的工作。

6.相关工作

我们的技术建立在许多相关工作的基础上;我们围绕(i)执行异步IO的框架,(ii)并行编程的抽象实现,(iii)基于消息传递的语言,以及(iv)取消异步IO的技术进行讨论。

异步IO框架。基于线程和基于回调的编程模型的优点经常被重新讨论。Lauer和Needham观察到,这些模型的特殊形式可以看作是对偶的:在一个模型中编写的程序与在另一个[23]中编写的程序本质上是相同的。Ousterhout认为线程在大多数情况下都不是一个好主意(就正确使用它们以及性能而言)[27]。Adya等人认为“线程”的概念合并了许多相关的概念,在不使用手动堆栈管理的情况下使用协作任务管理是有价值的[3];AC受到这个参数的启发,并将异步IO的管理与并行工作的管理分离开来。基于相关的争论,von Behren等人认为,对线程的许多批评实际上是由于早期实现[35]的糟糕性能。

有几种技术简化了异步IO,而无需重新移动手动的堆栈抓取。Dabek等人的libasync库帮助管理事件处理程序的状态,并提供处理程序[9]之间的并发控制。Elmeleegy等人提出,如果给定的调用可以立即完成,异步IO接口应该同步操作[11]。Cunningham和Kohler展示了如何重构异步IO接口,以帮助链接相关操作[8]。我们的工作与这些工作的不同之处在于,我们避免了翻页。

Grand central dispatch (GCD)通过线程池(http://developer。apple.com/technologies/mac/snowleopard/ gcd.html)。通常情况下,任务会一直运行到完成,并且在Objective c中被定义为闭包的一种形式。手动栈剥离可以通过在另一个闭包中嵌套一个闭包(捕获外部闭包的变量)来缓解。

一些系统允许异步IO程序在没有栈抓取的情况下被编写。原线程提供了一个以线程风格[10]编程嵌入式系统的系统。每个“线程”是一个单独的函数,它被分割成一系列事件处理程序。Fischer等人在TaskJava[12]中使用了一个更通用的版本,允许调用方等待事件。手动注释会导致TaskJava编译器用switch语句替换方法的主体,使其能够在中间点重新启动;变量访问被堆中的状态记录访问代替。Srinivasan和Mycroft在Kilim的实现中使用了这样的转换,Kilim是Java[31]的一个基于参与者的框架。我们修改运行时系统以避免这种转换的需要。

Haller和Odersky的Scala Actors[15]结合了基于线程和基于事件的编程模型;代码可以使用接收操作阻塞,也可以使用反应操作注册事件处理程序。

Tame提供了一组c++抽象,以线程风格编写异步IO。线程可能会在函数调用中阻塞;调用立即返回,并将指定的局部变量的内容存储到堆结构中。Tame提供了基于回调的同步原语,并使用引用计数来管理临时存储。CLARITY支持带有非阻塞调用的类线程模型和类似监视器的同步机制[6]。与异步调用一样,如果被调用方阻塞,则继续执行非阻塞调用。我们集成了取消,并提供了块结构的同步。

CPC[21]使用编译器将代码转换为延续传递风格(CPS),以支持大量线程:线程由堆分配的挂起记录的动态链组成。在AC中,IO库提供了与OS公开的异步IO操作的集成。AC避免了CPS转换的需要,并添加了用于创建、同步和取消异步工作的语言构造。

Li和Zdancewic在GHC Haskell[25]中结合了基于回调和基于线程的通信抽象。基于线程的操作以单元风格编写,以提供排序。调度程序强制计算每个线程的工作,直到下一次IO操作。Vouillon描述了Lwt,这是OCaml[37]的合作线程的实现。Lwt提供了执行单独AIO操作的原语线程,以及将这些原语链接在一起的绑定操作符。

Syme等人在f#语言[32]中提供了一个“异步模态”。类型为Async的值是一个可以异步运行并生成类型为T的值的计算,f#语言的核心控制流语法可以用来构造这些异步计算。Microsoft . net框架的第4版支持异步IO,无需手动栈抓取(http://www。microsoft.com/events/pdc/会话FT09)。异步执行的函数必须在其签名中包含async修饰符,并具有Task返回类型;然后可以挂起它们的执行,并使用为挂起状态分配的堆临时对象恢复执行。相反,AC允许执行异步IO的代码像往常一样编译,并直接使用栈上的临时数据来管理异步工作;这将带来更好的性能,正如我们在第5节中演示的那样。

线程和并行编程尽管我们保持AC抽象与那些表示并行性的抽象分离,但我们的设计和实现是建立在并行编程技术之上的。通过将这些技术应用到单个线程的计算中,我们获得了并发控制的简化(例如,我们隐式地在线程的堆栈中保留关于异步调用的延续的信息,因为这些信息只被线程本身访问)。Mohr引入了延迟任务创建[26]的使用,延迟创建新线程以执行计算,直到有空闲的处理器可用。我们的Clang/LLVM实现延迟创建一个单独的堆栈,直到被调用者阻塞。Gold- stein介绍了轻量级线程[14]的技术分类。我们对异步调用的处理类似于Goldstein术语中的“惰性断开”。CILK[13]实现使用了单独的函数克隆来包含或省略同步;我们不使用克隆,因为我们的顺序模型减少了同步的数量。和AC一样,CILK-4使用仙人掌堆。CILK-5使用堆分配帧。

StackThreads/MP提供了一种异步调用形式,其中被调用方可以被空闲处理器[33]窃取。偷窃是合作的(受害者对我们负责——等待被调用者的当前工作)。StackThreads/MP复用帧从多个线程超过一个堆栈;新的帧被分配到堆栈的顶部,但是出现的漏洞没有被填满。与AC一样,von Behren等人[36]的Capriccio系统在单个OS线程上复用io密集型工作负载。它提供了传统的基于线程的编程模型,而AC提供了块结构的结构来同步和取消异步IO。

X10语言提供了异步和完成结构,这在一定程度上启发了我们的设计[7]。在X10中,异步为并行处理器创建工作。例如,X10的构造不会提供AC的串行省略属性。Lee和Palsberg使用可以在并行线程中交叉工作的小步转换为Featherweight X10[24]定义了一个操作语义。AME提供了一个基于可序列化原子操作的编程模型。执行async将创建一个新的原子操作,该操作将在当前原子操作[19]之后运行C。

Sivaramakrishnan等人描述了一种轻量级线程的形式,称为“寄生线程”[29]。寄生线程在宿主线程上多路复用,多个寄生线程同时使用同一个宿主堆栈。静态分析和动态检查的结合用于防止来自不同寄生虫的帧之间的冲突。我们可以应用这些技术来进一步降低AC中栈的分配成本。

基于消息的语言:Hoare的CSP[17]启发了许多语言设计。与CSP不同的是,我们的核心语言提供了缓冲的发送/接收操作作为原语,并省略了“alt”风格的操作,以便在一组替代操作中发送/接收。我们的设计反映了目标操作系统中可用的原语。典型的操作系统接口不提供精确的语义,因为多个请求可以在不同的设备上并发完成。使用AC的程序员可以通过附加的软件层构建一个“alt”风格的操作。

和AC一样,Alef[39]和Go (golang.org)语言都在命令式设置中提供消息传递操作。Alef支持协同程序的协作调度,因为它们阻塞了通信操作。Go引入了一个“goroutine”抽象概念;它们在OS线程上多路复用(因此不同的例程可以并行运行),但是使用了分段堆栈实现。AC只关注在单个线程中构造异步IO操作,而不是使用多个OS线程。这种选择让我们保留了顺序编程模型。

与我们的工作同时,Ziarek等人在Concurrent ML[28]的一级事件抽象基础上开发了一个可组合异步事件[40]系统。Ziarek等人的异步事件在事件结构中封装了与异步动作关联的隐式线程创建,从而实现了异步协议的可组合构造。

取消。AC的块结构方法的取消是不同于以前的工作。Modula-2+[5]提供了一种“警报”机制,如果另一个线程在同步操作中被阻塞,该机制会导致在另一个线程中引发异常。Java提供了类似的机制。POSIX定义了一个“取消点”的概念,在这个点上一个线程的工作可以被另一个线程[1]取消。通常,这些是阻塞的系统调用。此外,POSIX异步IO提供了aio取消操作来取消单个指定的异步IO操作,或取消给定文件上的所有操作。Windows提供了一个“取消令牌”抽象;当一个令牌启动时,它可以被传递给一个异步IO操作,取消令牌将取消与它相关的所有异步请求。

7.结论

AC为IO提供了与基于本机回调的异步接口相当的性能,同时保留了同步操作的可组合编程风格。我们的总体观点是,异步IO应该支持通过使用抽象,如异步. . finish并取消形容异步的消息在一个普通的顺序的程序比通常开发一种新的技术,替代的IO国米——面临基于显式事件或回调。我们的结果表明,AC可以匹配基于回调的接口的性能。

Barrelfish的研究操作系统,包括AC,可以在http://barrelfish.org上找到。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值