Ada 编程/任务
一个任务单元是一个程序单元,它与 Ada 程序的其余部分同时执行。在 Ada 术语中,相应的活动,一个新的控制位置,称为任务,类似于线程,例如在 Java 线程 中。主程序的执行也是一个任务,匿名环境任务。一个任务单元既有声明,也有主体,这是必须的。一个任务主体可以作为子单元单独编译,但任务不能是库单元,也不能是泛型。每个任务都依赖于一个主控,它是直接包围的声明区域——一个块、一个子程序、另一个任务或一个包。主控的执行不会在所有依赖任务终止之前完成。环境任务是所有其他任务的主控;它只在所有其他任务终止时才终止。
任务单元类似于包,任务声明定义了从任务导出的实体,而它的主体包含任务的局部声明和语句。
一个任务的声明如下
taskSingleisdeclarations of exported identifiersendSingle; ...taskbodySingleislocal declarations and statementsendSingle;
如果没有任何导出,任务声明可以简化,因此
task No_Exports;
例 1
procedureHousekeepingistaskCheck_CPU;taskBackup_Disk;taskbodyCheck_CPUis...endCheck_CPU;taskbodyBackup_Diskis...endBackup_Disk; -- the two tasks are automatically created and begin executionbegin-- Housekeepingnull; -- Housekeeping waits here for them to terminateendHousekeeping;
可以声明任务类型,从而允许动态创建任务单元,并将其纳入数据结构
tasktypeTis...endT; ... Task_1, Task_2 : T; ...taskbodyTis...endT;
任务类型是受限的,也就是说,它们在受限类型中受到限制,因此不允许赋值和比较。
任务可以导出的唯一实体是入口。一个入口看起来很像一个过程。它有一个标识符,可以有in、out 或 in out 参数。Ada 通过入口调用支持任务之间的通信。信息通过入口调用的实际参数在任务之间传递。我们可以将数据结构封装在任务中,并通过入口调用对其进行操作,这与使用包封装变量的方式类似。主要区别在于入口由被调用任务执行,而不是调用任务,调用任务会暂停,直到调用完成。如果被调用任务尚未准备好服务入口的调用,则调用任务将在与入口关联的(FIFO)队列中等待。调用任务和被调用任务之间的这种交互称为会合。调用任务通过调用被调用任务的某个入口来请求与特定命名任务的会合。一个任务通过为入口执行accept 语句来接受与任何调用特定入口的调用者的会合。如果没有调用者在等待,它就会被挂起。因此,入口调用和 accept 语句的行为是对称的。(老实说,优化的目标代码可能会将上下文切换次数降低到比这个糟糕的描述中所暗示的次数更少。)
然而,过程和入口之间存在很大区别。一个过程只有一个主体,在被调用时执行。入口和相应的 accept 语句之间不存在这种关系。一个入口可以有多个 accept 语句,每次执行的代码可能不同。事实上,甚至可能根本不存在 accept 语句。(当然,调用这样的入口会导致调用者死锁,除非是定时的。)
例 2 以下任务类型实现了一个单槽缓冲区,即一个封装的变量,可以严格交替地插入和删除值。请注意,缓冲区任务不需要状态变量来实现缓冲区协议:插入和删除操作的交替由 Encapsulated_Buffer_Task_Type 主体中的控制结构直接强制执行,它通常是loop。
tasktypeEncapsulated_Buffer_Task_TypeisentryInsert (An_Item :inItem);entryRemove (An_Item :outItem);endEncapsulated_Buffer_Task_Type; ... Buffer_Pool :array(0 .. 15)ofEncapsulated_Buffer_Task_Type; This_Item : Item; ...taskbodyEncapsulated_Buffer_Task_TypeisDatum : Item;beginloopacceptInsert (An_Item :inItem)doDatum := An_Item;endInsert;acceptRemove (An_Item :outItem)doAn_Item := Datum;endRemove;endloop;endEncapsulated_Buffer_Task_Type; ... Buffer_Pool(1).Remove (This_Item); Buffer_Pool(2).Insert (This_Item);
为了避免在可以进行生产性工作时被挂起,服务器任务通常需要自由接受对多个备选入口的任何一个的调用。它通过选择性等待语句来做到这一点,该语句允许任务等待对两个或多个入口的任何一个的调用。
如果选择性等待语句中只有一个备选有挂起的入口调用,则接受该调用。如果两个或多个备选都有挂起的调用,则实现可以自由接受任何一个。例如,它可以选择一个随机的。这在程序中引入了有限的非确定性。一个健全的 Ada 程序不应依赖于用于选择挂起的入口调用的一种特定方法。(但是,如果需要,有一些设施可以影响使用的方法。)
例 3
tasktypeEncapsulated_Variable_Task_TypeisentryStore (An_Item :inItem);entryFetch (An_Item :outItem);endEncapsulated_Variable_Task_Type; ...taskbodyEncapsulated_Variable_Task_TypeisDatum : Item;beginacceptStore (An_Item :inItem)doDatum := An_Item;endStore;loopselectacceptStore (An_Item :inItem)doDatum := An_Item;endStore;oracceptFetch (An_Item :outItem)doAn_Item := Datum;endFetch;endselect;endloop;endEncapsulated_Variable_Task_Type;
x, y : Encapsulated_Variable_Task_Type;
创建两个类型为 Encapsulated_Variable_Task_Type 的变量。它们可以这样使用
it : Item; ... x.Store(Some_Expression); ... x.Fetch (it); y.Store (it);
同样,请注意,主体的控制结构确保在接受任何 Fetch 操作之前,必须通过第一个 Store 操作为 Encapsulated_Variable_Task_Type 提供一个初始值。
根据情况,服务器任务可能无法接受对选择性等待语句中具有接受备选的某些入口的调用。任何备选的接受可以通过使用保护来进行条件化,保护是布尔型 接受的前提条件。这使得编写类似监控器的服务器任务变得很容易,而无需显式信号机制或互斥。具有 True 保护的备选称为开放的。如果在执行选择性等待语句时没有备选是开放的,则会出错,这会引发 Program_Error 异常。
例 4
taskCyclic_Buffer_Task_TypeisentryInsert (An_Item :inItem);entryRemove (An_Item :outItem);endCyclic_Buffer_Task_Type; ...taskbodyCyclic_Buffer_Task_TypeisQ_Size :constant:= 100;subtypeQ_RangeisPositiverange1 .. Q_Size; Length : Naturalrange0 .. Q_Size := 0; Head, Tail : Q_Range := 1; Data :array(Q_Range)ofItem;beginloopselectwhenLength < Q_Size =>acceptInsert (An_Item :inItem)doData(Tail) := An_Item;endInsert; Tail := TailmodQ_Size + 1; Length := Length + 1;orwhenLength > 0 =>acceptRemove (An_Item :outItem)doAn_Item := Data(Head);endRemove; Head := HeadmodQ_Size + 1; Length := Length - 1;endselect;endloop;endCyclic_Buffer_Task_Type;
任务允许封装和安全使用变量数据,而无需任何显式互斥和信号机制。例 4 显示了编写服务器任务来安全地代表多个客户端管理本地声明的数据是多么容易。无需对对受管理数据的访问进行互斥,因为永远不会同时访问。但是,仅为了提供一些数据而创建任务的开销可能过高。对于此类应用程序,Ada 95 提供了受保护的模块,这些模块基于众所周知的计算机科学概念监控器。受保护模块封装了一个数据结构,并导出了在自动互斥下对其进行操作的子程序。它还提供客户端任务之间自动的、隐式的条件信号。同样,受保护模块可以是单个受保护对象,也可以是受保护类型,允许创建多个受保护对象。
受保护模块只能导出过程、函数和入口,它的主体只能包含过程、函数和入口的主体。受保护数据在它的规范中的private 之后声明,但只能在受保护模块的主体内访问。受保护的过程和入口可以读取和/或写入其封装的数据,并自动相互排除。受保护的函数只能读取封装的数据,因此可以在同一个受保护对象中并发执行多个受保护函数调用,并完全安全;但是受保护的过程调用和入口调用会排除受保护的函数调用,反之亦然。受保护对象导出的入口和子程序由其调用任务执行,因为受保护对象没有独立的控制位置。(老实说,优化的目标代码可能会将上下文切换次数降低到比这个简单的描述中所暗示的次数更少。)
类似于可选地具有保护的任务入口,受保护的入口必须具有一个屏障来控制准入。这提供了自动信号,并确保当接受受保护的入口调用时,其屏障条件为 True,因此屏障为入口主体提供了可靠的前提条件。屏障可以静态地为 true,那么入口总是开放的。
例 5 以下是一个简单的受保护类型,类似于例 2 中的 Encapsulated_Buffer 任务。
protectedtypeProtected_Buffer_TypeisentryInsert (An_Item :inItem);entryRemove (An_Item :outItem);privateBuffer : Item; Empty : Boolean := True;endProtected_Buffer_Type; ...protectedbodyProtected_Buffer_TypeisentryInsert (An_Item :inItem)whenEmptyisbeginBuffer := An_Item; Empty := False;endInsert;entryRemove (An_Item :outItem)whennotEmptyisbeginAn_Item := Buffer; Empty := True;endRemove;endProtected_Buffer_Type;
请注意,使用状态变量 Empty 的屏障如何确保消息交替插入和删除,以及如何确保不会尝试从空缓冲区中获取数据。所有这些都是在调用任务或受保护类型本身中没有显式信号或互斥构造的情况下实现的。
调用受保护入口或过程的符号与调用任务入口的符号完全相同。这使得用另一个实现来替换抽象类型的任何一个实现变得很容易,调用代码不受影响。
例 6 以下任务类型实现了 Dijkstra 的信号量 ADT,具有 FIFO 调度的恢复进程。只要不违反信号量不变性,该算法就会接受对 Wait 和 Signal 的调用。当这种情况临近时,对 Wait 的调用暂时会被忽略。
tasktypeSemaphore_Task_TypeisentryInitialize (N :inNatural);entryWait;entrySignal;endSemaphore_Task_Type; ...taskbodySemaphore_Task_TypeisCount : Natural;beginacceptInitialize (N :inNatural)doCount := N;endInitialize;loopselectwhenCount > 0 =>acceptWaitdoCount := Count - 1;endWait;oracceptSignal; Count := Count + 1;endselect;endloop;endSemaphore_Task_Type;
该任务可以用如下方式使用
nr_Full, nr_Free : Semaphore_Task_Type; ... nr_Full.Initialize (0); nr_Free.Initialize (nr_Slots); ... nr_Free.Wait; nr_Full.Signal;
或者,可以通过受保护的对象提供信号量功能,从而大幅提高效率。
例 7 此受保护类型的 Initialize 和 Signal 操作是无条件的,因此它们被实现为受保护的程序,但是 Wait 操作必须被保护,因此被实现为一个入口。
protectedtypeSemaphore_Protected_TypeisprocedureInitialize (N :inNatural);entryWait;procedureSignal;privateCount : Natural := 0;endSemaphore_Protected_Type; ...protectedbodySemaphore_Protected_TypeisprocedureInitialize (N :inNatural)isbeginCount := N;endInitialize;entryWaitwhenCount > 0isbeginCount := Count - 1;endWait;procedureSignalisbeginCount := Count + 1;endSignal;endSemaphore_Protected_Type;
与上面的任务类型不同,这并不能确保在 Wait 或 Signal 之前调用 Initialize,并且 Count 被赋予了一个默认的初始值。恢复任务版本的这种防御性功能留给读者作为练习。
有时我们需要一组相关的入口。由离散类型索引的入口族满足了这一需求。
例 8 此任务提供了一个包含多个缓冲区的池。
subtypeBuffer_IdisIntegerrange1 .. nr_Bufs; ...taskBuffer_Pool_TaskisentryInsert (Buffer_Id) (An_Item :inItem);entryRemove (Buffer_Id) (An_Item :outItem);endBuffer_Pool_Task; ...taskbodyBuffer_Pool_TaskisData :array(Buffer_Id)ofItem; Filled :array(Buffer_Id)ofBoolean := (others => False);beginloopforIinData'RangeloopselectwhennotFilled(I) =>acceptInsert (I) (An_Item :inItem)doData(I) := An_Item;endInsert; Filled(I) := True;orwhenFilled(I) =>acceptRemove (I) (An_Item :outItem)doAn_Item := Data(I);endRemove; Filled(I) := False;elsenull; -- N.B. "polling" or "busy waiting"endselect;endloop;endloop;endBuffer_Pool_Task; ... Buffer_Pool_Task.Remove(K)(This_Item);
注意,繁忙等待else null 在这里是必要的,以防止任务在没有针对它的挂起调用时被挂起在某个缓冲区上,因为这种挂起会延迟对所有其他缓冲区的请求(可能无限期地)。
服务器任务通常包含无限循环,以允许它们连续地为任意数量的调用提供服务。但是,在任务终止之前,控制权不能离开任务的主程序,因此我们需要一种方法让服务器知道它何时应该终止。这通过选择性等待中的终止备选来完成。
例 9
tasktypeTerminating_Buffer_Task_TypeisentryInsert (An_Item :inItem);entryRemove (An_Item :outItem);endTerminating_Buffer_Task_Type; ...taskbodyTerminating_Buffer_Task_TypeisDatum : Item;beginloopselectacceptInsert (An_Item :inItem)doDatum := An_Item;endInsert;orterminate;endselect;selectacceptRemove (An_Item :outItem)doAn_Item := Datum;endRemove;orterminate;endselect;endloop;endTerminating_Buffer_Task_Type;
任务在以下情况下终止:
- 至少有一个终止备选是打开的,并且
- 没有挂起的调用到它的入口,并且
- 相同主程序的所有其他任务都处于相同状态(或已经终止),并且
- 任务的主程序已完成(即,已执行完所有语句)。
条件 (1) 和 (2) 确保任务处于适合停止的状态。条件 (3) 和 (4) 确保停止不会对程序的其余部分产生不利影响,因为不可能再有可能会改变其状态的调用。
任务可能需要避免被调用到速度缓慢的服务器而被阻塞。计时入口调用允许客户端指定在实现 rendezvous 之前的最大延迟,如果超过此延迟,则尝试的入口调用将被撤回,并执行替代语句序列。
例 10
taskPassword_ServerisentryCheck (User, Pass :inString; Valid :outBoolean);entrySet (User, Pass :inString);endPassword_Server; ... User_Name, Password : String (1 .. 8); ... Put ("Please give your new password:"); Get_Line (Password);selectPassword_Server.Set (User_Name, Password); Put_Line ("Done");ordelay10.0; Put_Line ("The system is busy now, please try again later.");endselect;
要使任务提供的功能超时,需要两个不同的入口:一个用于传入参数,一个用于收集结果。在与后者的 rendezvous 超时将达到预期效果。
例 11
taskProcess_DataisentryInput (D :inDatum);entryOutput (D :outDatum);endProcess_Data; Input_Data, Output_Data : Datum;loopcollect Input_Data from sensors; Process_Data.Input (Input_Data);selectProcess_Data.Output (Output_Data); pass Output_Data to display task;ordelay0.1; Log_Error ("Processing did not complete quickly enough.");endselect;endloop;
对称地,选择性等待语句中的延迟备选允许服务器任务在实现与任何客户端的 rendezvous 时,在达到最大延迟后撤回接受调用的提议。
例 12
taskResource_LenderisentryGet_Loan (Period :inDuration);entryGive_Back;endResource_Lender; ...taskbodyResource_LenderisPeriod_Of_Loan : Duration;beginloopselectacceptGet_Loan (Period :inDuration)doPeriod_Of_Loan := Period;endGet_Loan;selectacceptGive_Back;ordelayPeriod_Of_Loan; Log_Error ("Borrower did not give up loan soon enough.");endselect;orterminate;endselect;endloop;endResource_Lender;
入口调用可以被设为条件调用,因此如果 rendezvous 未立即实现,则会撤回。这使用带有else部分的选择语句符号。因此,结构
selectCallee.Rendezvous;elseDo_something_else;endselect;
和
selectCallee.Rendezvous;ordelay0.0; Do_something_else;endselect;
在概念上似乎是等效的。但是,尝试启动 rendezvous 可能需要一些时间,尤其是当被调用者位于另一个处理器上时,因此delay 0.0;可能会过期,尽管被调用者能够接受 rendezvous,而else结构是安全的。
重新入队语句允许 accept 语句或入口体在完成时重定向到不同的或相同的入口队列,甚至重定向到另一个任务的入口队列。被调用入口必须共享相同的参数列表或无参数。原始入口的调用者不知道重新入队,并且入口调用尽管现在可能指向另一个任务的另一个入口,但仍然继续进行。
重新入队语句通常应该用于快速检查对实际工作的某些先决条件。如果这些条件得到满足,则实际工作将委托给另一个任务,因此调用者应几乎立即被重新入队。
因此,重新入队可能会对计时入口调用产生影响。更具体地说,假设计时入口调用指向 T1.E1,T1.E1 中的重新入队指向 T2.E2
taskbodyT1is...acceptE1do... -- Here quick check of preconditions.requeueT2.E2; -- delegationendE1; ...endT1;
设 Delta_T 为计时入口调用 T1.E1 的超时时间。现在有几种可能性
1. Delta_T 在 T1.E1 被接受之前过期。
- 入口调用被中止,即从队列中取出。
2. Delta_T 在 T1.E1 被接受之后过期。
- T1.E1 已完成(检查了先决条件)并且 T2.E2 将被接受。
- 对于不知道重新入队的调用者,入口调用仍在执行;它只在 T2.E2 完成时才完成。
因此,尽管原始入口调用可能被推迟很长时间,而 T2.E2 正在等待被接受,但从调用者的角度来看,调用正在执行。
要避免此行为,可以重新入队并中止调用。这改变了上面的情况 2
2.a 在 Delta_T 过期之前,将调用重新入队到 T2.E2。
- 2.a.1. T2.E2 在过期之前被接受,调用将继续直到 T2.E2 完成。
- 2.a.2. Delta_T 在 T2.E2 被接受之前过期:入口调用被中止,即从 T2.E2 的队列中取出。
2.b 在 Delta_T 过期之后,将调用重新入队到 T2.E2。
- 2.b.1. T2.E2 立即可用(即,没有重新入队),T2.E2 继续完成。
- 2.b.2. T2.E2 被入队:入口调用被中止,即从 T2.E2 的队列中取出。
简而言之,对于重新入队并中止,入口调用 T1.E1 在情况 1、2.a.1 和 2.b.1 中完成;它在 2.a.2 和 2.b.2 中被中止。
那么这三个入口有什么区别呢?
acceptE1do... -- Here quick check of preconditions.requeueT2.E2withabort; -- delegationendE1;acceptE2do... -- Here quick check of preconditions. T2.E2; -- delegationendE2;acceptE3do... -- Here quick check of preconditions.endE3; T2.E2; -- delegation
E1 刚刚讨论过。重新入队后,其包含的任务可以用于其他工作,而调用者仍然被挂起,直到其调用完成或中止。
E2 也是通过入口调用进行委托。因此,E2 仅在 T2.E2 完成时才完成。
E3 首先释放调用者,然后委托给 T2.E2,即入口调用使用 E3 完成。
FIFO、优先级、优先级反转避免……待完成。
此语言功能仅从Ada 2005开始可用。
任务和受保护类型也可以实现接口.
typePrintableistaskinterface;procedureInput (D :inPrintable);taskProcess_DataisnewPrintablewithentryInput (D :inDatum);entryOutput (D :outDatum);endProcess_Data;
为了允许多态性所需的委托,接口Printable应在其自己的包中定义。然后可以定义实现Printable接口的不同任务类型,并以多态方式使用这些实现
withprintable_package;useprintable_package; -- This package contains the definition of PrintableprocedurePrinteristasktypePrint_RedisnewPrintablewithend;tasktypePrint_BlueisnewPrintablewithend;taskbodyPrint_RedisbeginAda.Text_IO.Put_Line ("Printing in Red");endPrint_Red;taskbodyPrint_BlueisbeginAda.Text_IO.Put_Line ("Printing in Blue");endPrint_Blue; printer_task :accessPrintable'Class;beginprinter_task :=newPrint_Red; printer_task :=newPrint_Blue; -- Beware, this leaks memory. Example only.endPrinter;
此功能也称为同步接口。
Ada 任务功能太多,不适合某些应用程序。因此,对于某些应用(主要是安全关键或安全关键应用)存在限制和配置文件。限制和配置文件通过编译指示定义。限制禁止使用某些功能,例如 No_Abort_Statements 限制禁止使用 abort 语句。配置文件(不要与子程序的参数配置文件混淆)组合了一组限制。
参见13.12:编译指示限制和编译指示配置文件 [带注释的]
- 第 4 章:程序结构
- 第 6 章:并发
