ADA 95教程 高级特性 任务Tasking
什么是任务分配?
不管你有什么编程经验,任务分配的主题对你来说都可能是新的,因为任务分配是一种相对较新的技术,在大多数编程语言中是不可用的。如果您需要与大多数其他语言进行某种并行操作,则需要使用一些相当棘手的技术或用汇编语言编写驱动程序。然而,在Ada中,任务分配被设计成语言,非常容易使用。
但在ADA中并不完全相同
任务分配是指计算机即使只有一个处理器,也能同时处理两件或两件以上的事情。真正的并行操作发生在硬件中实际有多个处理器时,但是由于大多数现代计算机只有一个处理器,我们必须通过在两个或多个任务之间共享处理器使系统看起来有多个处理器。我们稍后会有更多的话要说,但我们必须先讨论另一个话题。
实时性要求定时
Example program ------> e_c26_p1.ada
-- Chapter 26 - Program 1 with Ada.Text_IO, Ada.Integer_Text_IO; use Ada.Text_IO, Ada.Integer_Text_IO; with Ada.Calendar; use Ada.Calendar; procedure Timer is package Fix_IO is new Ada.Text_IO.Fixed_IO(DAY_DURATION); use Fix_IO; Year,Month,Day : INTEGER; Start,Seconds : DAY_DURATION; Time_And_Date : TIME; begin Put_Line("Begin 3.14 second delay"); delay 3.14; Put_Line("End of 3.14 second delay"); Time_And_Date := Clock; Split(Time_And_Date, Year, Month, Day, Start); -- get start time for Index in 1..9 loop Put("The date and time are now"); Time_And_Date := Clock; Split(Time_And_Date, Year, Month, Day, Seconds); Put(Month, 3); delay 0.2; Put(Day, 3); delay 0.1; Put(Year, 5); delay 0.1; Put(Seconds - Start, 8, 3, 0); New_Line; delay 0.6; end loop; Put_Line("Begin non-accumulative timing loop here."); Time_And_Date := Clock; Split(Time_And_Date, Year, Month, Day, Start); -- get start time for Index in 1..9 loop Time_And_Date := Clock; Split(Time_And_Date, Year, Month, Day, Seconds); Put("The elapsed time is"); Put(Seconds - Start, 8, 3, 0); New_Line; delay DAY_DURATION(Index) - (Seconds - Start); end loop; Time_And_Date := Clock; for Index in 1..12 loop Time_And_Date := Time_And_Date + 0.4; delay until Time_And_Date; Put("Tick "); end loop; New_Line; end Timer; -- Result of Execution -- Begin 3.14 second delay -- End of 3.14 second delay -- The date and time are now 3 22 1997 0.000 -- The date and time are now 3 22 1997 1.090 -- The date and time are now 3 22 1997 2.140 -- The date and time are now 3 22 1997 3.180 -- The date and time are now 3 22 1997 4.230 -- The date and time are now 3 22 1997 5.270 -- The date and time are now 3 22 1997 6.320 -- The date and time are now 3 22 1997 7.360 -- The date and time are now 3 22 1997 8.400 -- Begin non-accumulative timing loop here -- The elapsed time is 0.000 -- The elapsed time is 1.100 -- The elapsed time is 2.030 -- The elapsed time is 3.020 -- The elapsed time is 4.040 -- The elapsed time is 5.030 -- The elapsed time is 6.020 -- The elapsed time is 7.010 -- The elapsed time is 8.000 -- Tick Tick Tick Tick Tick Tick Tick Tick Tick Tick Tick Tick
在Ada编程语言的初始设计中,要求它能够在实时环境中运行。这需要我们对时间有一定的控制。我们至少需要阅读当前时间并知道何时到达某个特定时间的能力。名为e_c26_p1.ada的示例程序将说明如何做到这一点。
程序以我们通常的方式开始,除了添加第5行中列出的新包Ada.Calendar 如果您有经过验证的Ada编译器,则必须随编译器提供的包。规范包Ada.Calendar 列在ADA95参考手册(ARM)的9.6节中,可能列在编译器文档的某个地方。此软件包使您能够读取系统时间和日期,并允许您设置定时延迟。请参阅规范包列表Ada.Calendar 在下一段的讨论中继续。
CLOCK函数
您将注意到类型TIME是私有的,因此您无法看到它是如何实现的,但是您不需要看到它。对函数Clock的调用将当前时间和日期返回给time类型的变量,并提供其他函数来获取日期的各个元素或自午夜起的秒数。不能直接读取单个元素,因为某些元素可能在后续读取之间发生更改,从而导致错误的数据。提供了一个名为Split的过程来拆分类型TIME变量并一次返回所有四个字段,另一个过程名为Time_Of,当给定四个元素作为输入时,它将把各个元素组合成一个TIME 类型变量。
最后,为了有效地使用Ada.Calendar 包裹。将声明一个异常,如果您试图错误地使用其中一个子程序,将引发该异常。
延迟声明
保留字delay 用于向计算机指示您希望在程序的某个点包含延迟。延迟以秒为单位给出,如正在研究的程序的第19行所示,并被声明为一个固定点数,由每个实现定义。延迟值的类型是DAY_DURATION,但在本例中,使用的是universal_real类型。编译器的附录M给出了延迟的确切定义。它必须允许至少0.0到86400.0的范围,这是一天中的秒数,并且它必须允许不超过20毫秒的delta 。请参阅编译器文档的附录M,以查看针对特定编译器的此类型的确切声明。ARM要求当遇到delay语句时,系统必须在发生点至少延迟delay语句中指定的时间段,但没有说明系统可以延迟多长时间。这将导致一些不准确的延误,这将由你照顾。我们稍后将在这个示例程序中看到如何实现。
延迟变量使用固定点变量,这样可以在不损失精度的情况下添加时间,并且固定点号具有固定精度。
当您执行此程序时,您将看到监视器上显示的第一行,然后由于第19行中的延迟语句,在显示第二条消息之前暂停。实际上,根据Ada规范,暂停时间至少为3.14秒。
使用时钟功能
在第22行中,Clock函数用于返回当前时间和日期,并将其分配给名为Time_And_Date的变量。在下一行中,我们使用名为Split的过程将时间和日期(包含在名为Time_And_Date的复合变量中)拆分为各个组件。尽管我们唯一感兴趣的组件是Seconds字段,但我们必须为其他每个字段提供一个变量,这仅仅是因为过程调用的性质。在函数调用中,我们分配秒的值以供以后使用。这是我们开始循环的时间记录,稍后将使用。根据日历包的定义,时间的形式是从午夜开始经过的秒数。
我们在第25行到第38行执行一个循环,读取时间和日期,将其拆分为各个组件,并显示每个组件。我们不显示从午夜开始的时间,而是从第35行的当前时间中减去开始时间,实际上我们使用了Ada.Calendar 包裹。我们显示执行这个程序的第22行以来经过的时间。最后,每次通过循环时,我们都会加上一秒钟的总延迟,这样我们就可以看到延迟的累积。
我们正在这个循环中积累错误
您可能还记得,delay语句至少需要列出的延迟量,但没有提到返回程序时允许的额外延迟,以便编译器编写者在如何实现delay语句方面有回旋余地。此外,我们需要一些时间来执行循环中的其他语句,因此当您编译和执行此程序时,时间不会在循环中每经过一次就提前一秒,而是会随着时间的推移而略微增加,这一点也不奇怪。
没有错误的循环
在第42行到第51行中,我们基本上重复了这个循环,但略有不同。我们不是在每个循环中延迟一秒钟,而是延迟到达所需时间点所需的量。在第50行中,我们使用显式类型转换将Index 类型转换为DAY_DURATION,然后减去当前经过的时间来计算所需的延迟时间。这样可以防止错误的累积,当您运行程序时,您将只看到由定点编号引入的数字化错误。然而,这种方法存在一个潜在的问题。
如果要求负延迟怎么办?
当这样计算延迟时间时,所需的延迟时间可能会导致负数。Ada设计者有足够的远见,在大多数应用程序中,您只需要向前推,因此他们定义了延迟,使得延迟时间的负值被解释为零,并且不会产生任何错误。如果负时间应该被认为是应用程序的错误条件,则由您来检测它并发出相应的错误消息,或者引发异常。
关于这个示例程序的最后一点是第56行所示的delay 语句。系统在此延迟,直到给定的时间类型为time的绝对时间。如果给定的时间早于执行此语句的时间,则不会有延迟。这个循环的其余部分很琐碎,所以您可以自己研究它。
一定要编译和执行这个程序,并观察输出。由于第一个循环中的延迟,输出到监视器的数据有些不规则,在执行程序时可以看到。
延迟不属于日历的一部分
在我们离开这个项目之前,必须提出最后一点。这个Ada.Calendar 包和delay 语句都是在这里介绍的,尽管它们一起工作得很好,但它们是完全分开的。延迟声明不属于Ada.Calendar 程序包,这将在本章后面的示例程序中得到证明。
我们的第一个任务示例
Example program ------> e_c26_p2.ada
-- Chapter 26 - Program 2 with Ada.Text_IO, Ada.Integer_Text_IO; use Ada.Text_IO, Ada.Integer_Text_IO; procedure Task1 is task First_Task; task body First_Task is begin for Index in 1..4 loop Put("This is in First_Task, pass number "); Put(Index, 3); New_Line; end loop; end First_Task; task Second_Task; task body Second_Task is begin for Index in 1..7 loop Put("This is in Second_Task, pass number"); Put(Index, 3); New_Line; end loop; end Second_Task; task Third_Task; task body Third_Task is begin for Index in 1..5 loop Put("This is in Third_Task, pass number "); Put(Index, 3); New_Line; end loop; end Third_Task; begin Put_Line("This is in the main program."); end Task1; -- Result of Execution -- This is in Third_Task, pass number 1 -- This is in Third_Task, pass number 2 -- This is in Third_Task, pass number 3 -- This is in Third_Task, pass number 4 -- This is in Third_Task, pass number 5 -- This is in Second_Task, pass number 1 -- This is in Second_Task, pass number 2 -- This is in Second_Task, pass number 3 -- This is in Second_Task, pass number 4 -- This is in Second_Task, pass number 5 -- This is in Second_Task, pass number 6 -- This is in Second_Task, pass number 7 -- This is in First Task, pass number 1 -- This is in First Task, pass number 2 -- This is in First Task, pass number 3 -- This is in First Task, pass number 4 -- This is in the main program.
检查名为e_c26_p2.ada的文件中包含一些任务的示例程序。首先要检查的是主程序,它由第38行中的一个可执行语句组成,该语句向监视器输出一行文本。它没有调用声明部分中的任何代码,这看起来可能很奇怪,但我们将看到,它不必这样做。
Ada任务由任务规范和任务体组成,前者在第7行中说明,后者在第8到15行中说明。这是第一个任务,两部分都从保留字task开始。任务的结构与子程序或包的结构非常相似。第一个例子是一个非常简单的任务,它执行一个包含输出语句的for循环。最终结果由监视器上显示的四行文本组成。
任务规范
任务规范可能比这个更复杂,但这是一个很好的第一个例子。任务规范的总体结构如下所示:
task <task-name> is <entry-points>; end <task-name>;
但如果没有入口点,则只需要保留字task后跟任务名称。与包一样,任务规范必须放在任务体之前。
任务体
任务正文以保留字task和body开头,后跟任务名称,其余结构与过程相同。有一个声明性部分,其中可以声明类型、变量、子程序、包,甚至其他任务,然后是可执行部分,该部分遵循与主程序相同的规则。在本例中,没有声明性部分,只有可执行部分。当这个任务被执行时,它将输出四行到监视器并停止。当这个任务在几分钟内执行时,我们将对它进行更多的介绍。
还有两个任务
您应该清楚,在这个程序中有两个额外的任务,一个在第17行到第25行,另一个在第27行到第35行,每个都有各自的任务规范和任务体。实际上有四个任务,因为主程序本身就是另一个与这里显式声明的三个任务并行运行的任务。由于了解各种任务的执行顺序非常关键,因此我们将花一些时间详细定义它。
Ada总是使用线性声明,当它加载任何程序时,它都会按照列表中给出的顺序加载。因此,当它发现任务主体从第8行开始时,它会详细说明它的所有声明,尽管在本例中没有声明,但它会加载任务的可执行部分,但还没有开始执行。它有效地使任务在第9行的begin处等待,直到其他任务准备好开始执行。它对名为Second_Task的任务和Third_Task的任务执行相同的操作。当所有声明都被详细说明后,它到达第37行主程序的开始处。此时,所有四个任务都在各自的begin语句处等待,并且允许所有四个任务同时开始执行。所有人都有同等的优先权,但当他们开始执行时,一件奇怪的事情发生了。
ADA未定义执行顺序
ARM定义的执行规则没有要求执行任何形式的时间切片,也没有要求实现允许Starving发生是非法的。Starving是指一个任务使用所有可用时间,而另一个或多个任务由于没有时间进行操作而被允许Starving。此外,对于我们示例中的四个任务中的哪一个将首先执行,没有要求的执行顺序,因为我们没有包含任何形式的优先级。由于这些规则,我们无法准确预测您的实现将做什么,但只能给出在一个特定的已验证Ada编译器上执行此程序的结果。如程序后面列出的执行结果所示,此特定编译器允许名为Third_Task 的任务使用所有计算能力,直到它运行到完成为止,然后Second_Task 使用所有资源,接着是First_Task,最后是主程序运行。您的特定编译器可能会以不同的顺序执行这些语句,但这仍然是正确的。唯一的要求是,所有17行输出任何顺序。当然,输出的顺序是根据顺序操作规则在任何任务中定义的。例如,Third_Task 的pass 2必须跟在同一个任务的pass 1之后。
任务如何结束?
当名为Third_task的任务到达第35行的end语句时,它已经执行了所有必需的语句,并且没有其他事情要做,所以它只是在那里等待,直到其他任务完成。当所有四个任务(包括主程序任务)都到达各自的结束语句时,Ada系统知道没有其他事情可做,因此它会像在任何正常程序完成时一样返回到操作系统。
由于这个程序的运行方式,不清楚所有四个任务是并行运行的,但它们实际上是并行运行的,我们将在下一个示例程序中看到。编译并执行这个程序,研究编译器的输出,看看它是否与我们编译器的输出不同。
增加延迟增加了操作的清晰度
Example program ------> e_c26_p3.ada
-- Chapter 26 - Program 3 with Ada.Text_IO, Ada.Integer_Text_IO; use Ada.Text_IO, Ada.Integer_Text_IO; procedure Task2 is task First_Task; task body First_Task is begin for Index in 1..4 loop delay 2.0; Put("This is in First_Task, pass number "); Put(Index, 3); New_Line; end loop; end First_Task; task Second_Task; task body Second_Task is begin for Index in 1..7 loop delay 1.0; Put("This is in Second_Task, pass number"); Put(Index, 3); New_Line; end loop; end Second_Task; task Third_Task; task body Third_Task is begin for Index in 1..5 loop delay 0.3; Put("This is in Third_Task, pass number "); Put(Index, 3); New_Line; end loop; end Third_Task; begin -- for Index in 1..5 loop -- delay 0.7; Put_Line("This is in the main program."); -- end loop; end Task2; -- Result of Execution (with comments in main program) -- This is in the main program. -- This is in Third_Task, pass number 1 -- This is in Third_Task, pass number 2 -- This is in Third_Task, pass number 3 -- This is in Second_Task, pass number 1 -- This is in Third_Task, pass number 4 -- This is in Third_Task, pass number 5 -- This is in First Task, pass number 1 -- This is in Second_Task, pass number 2 -- This is in Second_Task, pass number 3 -- This is in First Task, pass number 2 -- This is in Second_Task, pass number 4 -- This is in Second_Task, pass number 5 -- This is in First Task, pass number 3 -- This is in Second_Task, pass number 6 -- This is in Second_Task, pass number 7 -- This is in First Task, pass number 4 -- Result of Execution (with main program comments removed) -- This is in Third_Task, pass number 1 -- This is in Third_Task, pass number 2 -- This is in the main program. -- This is in Third_Task, pass number 3 -- This is in Second_Task, pass number 1 -- This is in Third_Task, pass number 4 -- This is in the main program. -- This is in Third_Task, pass number 5 -- This is in First Task, pass number 1 -- This is in Second_Task, pass number 2 -- This is in the main program. -- This is in the main program. -- This is in Second_Task, pass number 3 -- This is in the main program. -- This is in First Task, pass number 2 -- This is in Second_Task, pass number 4 -- This is in Second_Task, pass number 5 -- This is in First Task, pass number 3 -- This is in Second_Task, pass number 6 -- This is in Second_Task, pass number 7 -- This is in First Task, pass number 4
检查下一个名为e_c26_p3.ada的示例程序,您将注意到每个循环中都添加了延迟语句。选择延迟是为了说明每个环路实际上是并行运行的。如果忽略注释掉的语句,则主程序与上一个程序相同,并且由于主程序在其输出语句之前没有延迟,因此它将是第一个输出到监视器的程序。主程序也将首先完成它的操作,然后耐心地等待它的结束语句,直到其他三个任务完成它们的工作。
由于延迟的不同,这三个任务将分别在不同的时间完成它们的延迟,并且输出应该是非常可预测的。如果您检查程序末尾给出的执行结果,您将看到这三个任务实际上是并行运行的,正如我们前面所述。这将是有益的,你编译和执行这个程序,让你可以观察到第一手的输出。
主程序呢?
再次返回名为e_c26_p3.ada的程序,以便检查主程序。您应该从主程序中的三条语句中删除注释标记,以便它还包含一个循环和循环中每个传递的延迟。当编译并执行修改后的程序时,它将在第一次输出之前执行一个延迟,并将总共五行输出到监视器,与其他任务的输出交错。这应该是一个很好的迹象,表明主程序的行为就像另一个任务。编译并执行修改后的程序,以查看主程序的行为与另一个任务一样。
块中可以有任务
Example program ------> e_c26_p4.ada
-- Chapter 26 - Program 4 with Ada.Text_IO; use Ada.Text_IO; procedure Task3 is begin Put_Line("This is in the main program."); declare task First_Task; task Second_Task; task Third_Task; task body First_Task is begin for Index in 1..4 loop delay 0.0; Put_Line("This is in First_Task."); end loop; end First_Task; task body Second_Task is begin for Index in 1..4 loop delay 0.0; Put_Line("This is in Second_Task."); end loop; end Second_Task; task body Third_Task is begin for Index in 1..8 loop delay 0.0; Put_Line("This is in Third_Task."); end loop; end Third_Task; begin delay 0.0; Put_Line("This is in the block body."); delay 0.0; Put_Line("This is also in the block body."); end; -- of declare Put_Line("This is at the end of the main program."); end Task3; -- Result of Execution -- This is in the main program. -- This is in First_Task. -- This is in Second_Task. -- This is in the block body. -- This is in Third_Task. -- This is in First_Task. -- This is in Second_Task. -- This is also in the block body. -- This is in Third_Task. -- This is in First_Task. -- This is in Second_Task. -- This is in Third_Task. -- This is in First_Task. -- This is in Second_Task. -- This is in Third_Task. -- This is in Third_Task. -- This is in Third_Task. -- This is in Third_Task. -- This is in Third_Task. -- This is at the end of the main program.
检查名为e_c26_p4.ada的程序,以获取嵌入任务的主程序的示例。在主程序的第11行到第44行中声明了一个块,它与任何其他块一样以内联方式执行。然而,这个块有三个相同的任务,我们已经用在前面的程序中嵌入它。您会注意到这里的声明顺序是不同的,因为三个任务规范在第12行到第14行中一起给出,但是这是完全合法的,并且在最后两个程序中也是合法的。唯一的要求是,必须在每个已声明任务的任务体之前给出任务规范。
零延迟有什么好处?
你会注意到所有的延迟都是零时间的,这可能会让你问为什么我们还要费心把延迟包括进来。每次系统遇到任何类型的延迟时,它都必须查看每个任务,以查看是否有任何任务已超时并需要执行。当它这样做时,它可以按顺序前进到下一个任务,但不需要这样做。ARM没有定义下一个任务将选择哪个任务,因此下一个任务可以执行,包括当前正在执行的任务。最终结果是,所有四个任务(包括块的可执行部分中的任务)都可以以循环方式执行,并且没有一个任务是starved的。根据ARM的说法,一个任务让其他任务starve 是完全合法的,因此如果编译器允许这样做,那就不是一个错误。在实际编程环境中使用的任务分配不会有这个问题,因为我们将在本教程的下几章中讨论其他构造。
以类似于其他示例程序的方式,当所有四个任务都在其端点时,块被完成,剩余语句在主程序中执行。请注意,主程序没有进入在块中执行的任务。内联块作为主程序的一部分作为另一个顺序语句执行。因此,执行第9行,然后执行第11到44行中的块。当所有四个任务(包括块体中的一个)都完成时,第46行中的输出语句被允许执行。
编译并执行此程序并研究结果。使用结果向自己证明主程序第9行和第46行中的语句没有进入任务。
编程练习
编写一个过程,该过程可以从名为e_c26_p1.ada的程序中的任意位置调用,该程序将使用从名为Split的过程返回的秒字段,并以小时、分钟和秒为单位显示一天中的时间。从程序中的几个地方调用此过程。(Solution)
修改名为e_c26_p4.ada的程序,将第9行和第46行中的可执行语句包含在循环中,并带有延迟,以证明这些语句是按顺序执行的,不会进入仅限于第11行到第44行中的块的任务。(Solution)
---------------------------------------------------------------------------------------------------------------------------
原英文版出处:https://perso.telecom-paristech.fr/pautet/Ada95/a95list.htm
翻译(百度):博客园 一个默默的 *** 的人