并行加入多核嵌入式计算的并发

今日头条

1146人已加入

描述

对于我们许多人来说,术语并行性 和并发性几乎是同义词,即使我们认为它们代表的概念有所不同,我们也不完全同意使一个结构并行而另一个并行的原因。但是,随着我们进入多核,多核和基于GPU的计算时代,区分这两者,并观察这些差异如何影响嵌入式程序员的世界是很有用的。

并发编程在嵌入式系统
中有着悠久的历史,在这种情况下,它代表了使用单独的控制线程来管理发生的多种活动的方法。这些控制线程通常具有不同的相对优先级,其中中断处理程序抢占了非中断代码,而要求更高调用频率或更早截止日期的代码抢占了具有更低频率或更晚截止时间的代码。对于控制具有相同优先级的活动的控制线程,通常使用循环调度方法来确保没有活动被忽略太长时间。所有这些都不需要多个物理处理器,并且可以将其总体上视为需要管理许多活动时以适当方式共享有限资源的一种方式。通过允许将任何给定的控制线程限制为管理单个活动,它也可以看作是简化编程逻辑的一种方法。因为引入并发的控制线程可以简化整个程序的逻辑,所以支持这种方法的语言结构本身也较重就可以了。

与并发编程相反,并行编程通常更多地是通过使用整体分而治之策略将其分成多个部分来解决计算密集型问题,以便更好地利用多种处理资源来解决单个问题。引入并行编程而不是简化逻辑,通常会使逻辑变得更复杂,因此,重要的是,从语法上和运行时,并行编程结构的重量应非常轻,否则,其复杂性和运行时开销可能会超过潜在的加速。

调度线程
对于并行编程和并行编程,物理处理器的实际数量可能在程序的一次运行与下一次运行之间有所不同,因此通常在物理处理器之上提供一个抽象级别,通常由调度程序提供。对于并发编程,调度程序通常使用抢占,其中一个线程在执行过程中被中断,以允许更高优先级的线程接管共享的处理资源。对于并行编程,更多情况下,目标只是在最短的时间内完成所有工作,因此不经常使用抢占,而总吞吐量则成为成功的关键标准。

对于用于管理具有不同关键程度的外部活动的并发编程,实时调度程序通常依赖于使用某种速率单调或期限单调分析分配的优先级,以确保所有线程都将满足其期限[3]。相反,对于并行编程,一种称为工作窃取的方法(图1)作为一种平衡多个物理处理器上的负载的健壮方法而出现,同时为给定处理器提供了良好的引用局部性,并实现了处理器之间的良好访问分离。

嵌入式设计

图1:使用双端队列的工作窃取方法

工作窃取[2]基于一些相对简单的概念,但通常需要对底层调度程序进行非常仔细的编码,以实现所需的低开销。基本思想是,编译器将计算分解为非常轻量级的线程(我们将其称为picothreads),并且在调度程序中,每个物理处理器都有一台服务器,该服务器具有自己的双端队列(通常称为双端队列)。微微线程(图1)。
典型的微线程可能表示执行循环的单个迭代,或评估单个子表达式。微微线程会自动放置在生成双微线程的服务器的双端队列的尾部,并且当服务器完成给定微微线程的工作时,它将最近添加到自己双端队列的微微线程(从尾部)移除,并开始运行一。后进先出(LIFO)学科将双端队列有效地用作工作堆。
在某个时候,服务器的双端队列为空,它已经完成了所执行的总体计算。那时,服务器从其他服务器之一窃取了微微线程。但是在这种情况下,它将删除其他服务器双端队列的最旧的微线程。也就是说,为了进行窃取,使用了先进先出(FIFO)规则。这完成的是,当服务自己的双端队列时,服务器正在拾取一个微微线程,该微微线程可能正在处理关联处理器最近正在使用的数据,而当从另一个双端队列中窃取时,它将选择一个已经微弱的微微线程。在其双端队列上,可能将处理不在任何处理器的缓存中的数据,并且可能与其他任何处理器所处理的数据在物理上不很接近。
单个微线程足够小,因此通常无需在它们完成之前抢占它们,这意味着它们不需要自己的完整堆栈-它们可以共享服务器提供的堆栈。要过早终止总体并行计算,通常足以阻止新的微线程启动,而无需在执行过程中中断给定的微线程。实际上,它共享一个堆栈并使用运行到完成的方法,这些方法使这些微微线程相对于并发编程中使用的典型的可抢占的较重线程而言如此轻巧。

工作窃取有许多微妙之处,这仍然是一个活跃的研究领域,包括确定要从哪个服务器窃取,确定一次要窃取多少微微线程,选择有效的双端队列以支持一端的独占访问。以及如何从其他线程共享访问权限,以及如何处理微微线程之间的任何同步等。尽管如此,工作窃取的基本方法已被广泛采用,包括各种并行语言(Cilk +,Go,Rust,ParaSail等)。以及各种库(Java fork / join库,OpenMP,英特尔的线程构建模块等)。

那么,这一切与移动和嵌入式编程世界有何关系?事实是,多核硬件也已进入移动和嵌入式
领域,部分原因是多核体系结构通常可以提供最佳的每瓦性能,而功率几乎始终是这些资源受限环境中的主要问题。但是,这些环境中的大多数仍将具有硬性或软性的实时要求,因此仅靠窃取工作就无法提供这些要求。需要将更传统的并发编程方法与基于工作窃取的调度的某些方面进行仔细集成。

将实时与窃取工作相结合是一个新的研究领域,只有很少的学术论文关注此问题。尽管如此,它显然变得越来越重要。正在更新诸如ARINC 653之类的标准,该标准定义了用于混合关键性系统的强分区体系结构,以适应多核硬件。由于担心共享单个芯片的处理器不像强分区所要求的那样独立,因此采用的一种方法涉及将多个内核分配给单个分区用于其时间片,然后在完成时间片后将它们全部重新分配给不同的分区。这将允许使用混合工作窃取方法,其中每个分区都有自己的服务器进程集,每个服务器进程都有自己的双端队列(图2)。分割的时间片结束后,与该分区关联的所有服务器进程都将被挂起,并继续执行下一个分区的服务器进程。因此,这里我们抢占了服务器进程,而各个微微线程仍然可以通过将服务器进程视为一种虚拟处理器来使用运行完成模型。

嵌入式设计

嵌入式设计

嵌入式设计

图2:将实时和工作指导与强分区架构相结合

通过为每个实时优先级创建单独的服务器进程,可以采用类似的方式安排优先级调度。每个服务器都有自己的专用堆栈,只有当内核上所有优先级较高的服务器进程无关时,才会在内核上运行优先级较低的服务器进程。当实时需求较弱时,另一种选择是每个核心仅使用一个服务器进程,但对于不同的优先级使用单独的设备。使用这种方法,不会发生抢占picothread的抢占。但是,当服务器进程选择要执行的新微微线程时,将遵循优先级:服务器将首先从其自己的最高优先级非空双端队列中选择,但是如果另一台服务器具有比任何其他服务器更高优先级的非空双端队列,则从另一台服务器进行窃取。服务器自己的非空双端队列。

并发和并行的编程语言构造
许多编程语言都包含一些并发线程,互斥锁以及同步信号和等待的概念。PerBrinch Hansen的并发Pascal [1]是将许多这些概念整合到语言本身的最早的语言之一。Ada和Java从一开始就也包含并发编程概念,现在许多其他语言也包含这些概念。通常,并发线程的执行对应于近似执行命名函数或过程的异步执行。在InAda中,这是关联任务的任务主体。在Java中,它是关联的Runnable对象的run方法。锁通常与某种同步对象(通常称为监视器)相关联,对象上的某些或全部操作会在启动操作时自动获得锁,并在完成时自动释放锁,从而确保始终保持平衡。在Ada中,这些称为受保护的对象和操作,而在Java中,它们是类的同步方法。

信令和等待用于处理并发线程需要进行通信或以其他方式合作的情况,并且一个线程必须等待一个或多个其他线程采取某种操作或发生某些外部事件,然后才能进一步进行处理。信号发送和等待通常也由异步对象来实现,线程正在等待对象状态的某些变化,信号被用来指示状态已更改,并且一些等待线程应该重新检查以查看同步对象是否现在处于所需的状态。Hoare和Brinch Hansen提出的条件临界区代表了第一种语言构造之一,它基于布尔表达式隐式提供了这种等待和信号传递。通常,这是通过对对象或条件队列的显式Wait和Signal操作提供的(在Java中,信令使用notify或notifyAll)。Ada结合了条件关键区域的概念,并通过将带有入口屏障的条目合并到受保护的对象构造中来进行监视,从而无需显式的Signal和Wait调用。所有这些概念都代表了并发编程结构的含义。

相比之下,尽管正在迅速变化,但数量较少的语言却没有包含可以被认为是并行编程结构的语言。与并发编程一样,并行编程可以由显式语言扩展,标准库或二者的某种混合来支持。并行编程的第三个选项是使用程序注释,例如编译指示,向编译器提供方向,以允许编译器自动并行化原始顺序算法。

区分并行编程的一个特征是,并行计算的单位通常可以小于前奏函数或过程的执行,但可以表示循环的一个或多个迭代,或对较大表达式的一部分进行求值。此外,编译器和底层运行时系统更多地参与确定代码的哪些部分可以实际并行运行。这与传统的并发编程构造完全不同,传统的并发编程构造依赖于显式的程序员决策来确定线程边界在哪里。

Cilk是最早使用的具有通用并行编程结构的语言,它是由MIT的Charles Leiserson [3]设计的,现在由Intel作为其Intel Parallel Studio的一部分提供支持。Cilk允许程序员 在算法的战略要点插入诸如cilk_spawn 和cilk_sync之类的指令,其中_spawn 导致将表达式的评估分叉到单独的轻量级线程中,而_sync导致程序等待本地产生的并行线程,因此可以使用它们执行的结果。此外,Cilk还提供了使用cilk_for的功能 而不是简单地表示给定的for循环的迭代是并行执行的候选对象。现在提供类似功能的其他语言包括OpenMP,它使用编译指示而不是语言扩展来指导并行执行的插入; Google的languageGo,它包括用于具有通信通道的并行执行的轻量级goroutine,以及Mozilla Research的Rust,它支持大量轻量级的语言。使用所有权转移来避免竞争情况的任务通信,以及AdaCore的ParaSail语言使用基于无指针,无别名方法的安全自动并行化,该方法简化了分而治之的算法。

所有这些并行语言或扩展都采用了某种形式的窃取工作来调度其轻量级线程。所有这些语言使从顺序导向的思维方式转换为并行导向的思维方式变得更加容易。嵌入式和移动程序员现在应该开始使用这些语言进行实验,以准备将实时优先级功能与偷窃计划程序合并在一起,以提供先进的嵌入式和移动应用程序在绘图板上所需的反应性和吞吐量的结合,从而为未来的将来做好准备。lw

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分