Ada 样式指南/并发
并发可以是表面的并发或真正的并发。在单处理器环境中,表面并发是并发活动交错执行的结果。在多处理器环境中,真正的并发是并发活动重叠执行的结果。
并发编程比顺序编程更难,也更容易出错。Ada 的并发编程功能旨在简化并发程序的编写和维护,使其行为一致且可预测,并避免死锁和饥饿等问题。语言功能本身无法保证程序具有这些理想属性。它们必须以纪律和谨慎的方式使用,本节中的指南支持这一过程。
正确使用 Ada 并发功能可以产生可靠、可重用和可移植的软件。受保护的对象(在 Ada 95 中添加)封装并提供对其私有数据的同步访问(Rationale 1995,§II.9)。受保护的对象可以帮助您管理共享数据,而不会产生性能损失。任务模拟并发活动并使用 rendezvous 来同步协作的并发任务。在任务之间所需的大部分同步都涉及数据同步,通常可以使用受保护的对象最有效地实现。语言功能的误用会导致不可验证且难以重用或移植的软件。例如,使用任务优先级或延迟来管理同步是不可移植的。同样重要的是,可重用组件不应对任务执行的顺序或速度(即,对编译器的任务实现)做出假设。
虽然任务和受保护的对象等并发功能由 Ada 核心语言支持,但在与没有专门支持 Annex D(实时系统)的实现一起使用这些功能时,应谨慎。如果未专门支持 Annex D,则实时应用程序所需的特性可能未实现。
本节中的指南经常用“考虑...”的措辞,因为硬性规则不适用于所有情况。您在特定情况下做出的具体选择涉及设计权衡。这些指南的理由旨在让您了解其中一些权衡。
许多问题自然映射到并发编程解决方案。通过了解和正确使用 Ada 语言并发功能,您可以生成在很大程度上独立于目标实现的解决方案。任务在 Ada 语言中提供了一种方法来表达并发、异步控制线程,并为程序员减轻显式控制多个并发活动的负担。受保护的对象作为构建块来支持其他同步范式。任务协作执行软件所需的功能。单个任务之间需要同步和互斥。Ada 的 rendezvous 和受保护的对象提供了强大的机制来实现同步和互斥。
- 考虑使用受保护的对象来提供对数据的互斥访问。
- 考虑使用受保护的对象来控制或同步对多个任务共享数据的访问。
- 考虑使用受保护的对象来实现同步,例如被动资源监视器。
- 考虑将受保护的对象封装在包的私有部分或主体中。
- 考虑使用受保护的过程来实现中断处理程序。
- 如果硬件中断的最大优先级高于分配给处理程序的优先级上限,则不要将受保护的处理程序附加到该硬件中断。
- 避免在入口屏障中使用全局变量。
- 避免使用带有副作用的屏障表达式。
generic
type Item is private;
Maximum_Buffer_Size : in Positive;
package Bounded_Buffer_Package is
subtype Buffer_Index is Positive range 1..Maximum_Buffer_Size;
subtype Buffer_Count is Natural range 0..Maximum_Buffer_Size;
type Buffer_Array is array (Buffer_Index) of Item;
protected type Bounded_Buffer is
entry Get (X : out Item);
entry Put (X : in Item);
private
Get_Index : Buffer_Index := 1;
Put_Index : Buffer_Index := 1;
Count : Buffer_Count := 0;
Data : Buffer_Array;
end Bounded_Buffer;
end Bounded_Buffer_Package;
------------------------------------------------------------------
package body Bounded_Buffer_Package is
protected body Bounded_Buffer is
entry Get (X : out Item) when Count > 0 is
begin
X := Data(Get_Index);
Get_Index := (Get_Index mod Maximum_Buffer_Size) + 1;
Count := Count - 1;
end Get;
entry Put (X : in Item) when Count < Maximum_Buffer_Size is
begin
Data(Put_Index) := X;
Put_Index := (Put_Index mod Maximum_Buffer_Size) + 1;
Count := Count + 1;
end Put;
end Bounded_Buffer;
end Bounded_Buffer_Package;
受保护对象旨在提供一种“轻量级”机制,用于互斥和数据同步。只有在需要显式引入新的并发控制线程时,才应使用任务(请参阅指南 6.1.2)。
受保护对象提供了一种低开销、高效的方式来协调对共享数据的访问。受保护类型声明类似于程序单元,包含规范和主体。要保护的数据必须在规范中声明,以及用于操作此数据的操作。如果某些操作仅在满足条件时才允许执行,则必须提供入口。Ada 95 规则要求在受保护对象上的过程调用和入口调用结束时评估入口屏障。入口屏障应避免引用全局变量,以避免违反受保护对象状态的基本假设。受保护过程和入口应用于更改受保护对象的状态。
抽象的大多数客户端不需要知道它是如何实现的,无论是常规抽象还是共享抽象。受保护类型本质上是有限类型,可以使用受保护类型来实现由包导出的有限私有类型。如指南 5.3.3 中所述,抽象最好使用私有类型(可能从受控类型派生)或有限私有类型来实现,提供适当的操作来克服使用私有类型带来的限制。
基本原理(1995,第 9.1 节)描述了使受保护过程成为推荐的构建块的中断处理功能
受保护过程非常适合充当中断处理程序,原因有以下几点:它们通常都具有短的有限执行时间,不会任意阻塞,上下文有限,最后它们都需要与优先级模型集成。非阻塞临界区符合中断处理程序的要求,以及非中断级代码与中断处理程序同步的要求。入口屏障结构允许中断处理程序通过更改受保护对象组件的状态来向普通任务发出信号,从而使屏障变为真。
当使用受保护过程进行中断处理时,必须确保处理程序的优先级上限至少与要处理的中断的最大可能优先级一样高。使用优先级上限锁定,如果中断的优先级高于处理程序的优先级上限,将导致执行错误 (Ada 参考手册 1995,第 C.3.1 节 [带注释的])。
全局变量可能会被另一个任务更改,甚至会被受保护函数的调用更改。这些更改不会立即生效。因此,您不应在入口屏障中使用全局变量。
屏障表达式中的副作用会导致不良依赖。因此,您应避免使用会导致副作用的屏障表达式。
另请参阅指南。
如果包含受保护对象的抽象的客户端必须使用带有入口调用的 select 语句,则必须在包接口上公开受保护对象。
- 使用任务来模拟问题域中选定的异步控制线程。
- 考虑使用任务来定义并发算法。
- 如果应用程序需要同步无缓冲通信,请考虑使用 rendezvous。
问题域中的自然并发对象可以建模为 Ada 任务。
-- The following example of a stock exchange simulation shows how naturally
-- concurrent objects within the problem domain can be modeled as Ada tasks.
-------------------------------------------------------------------------
-- Protected objects are used for the Display and for the Transaction_Queue
-- because they only need a mutual exclusion mechanism.
protected Display is
entry Shift_Tape_Left;
entry Put_Character_On_Tape (C : in Character);
end Display;
protected Transaction_Queue is
entry Put (T : in Transaction);
entry Get (T : out Transaction);
function Is_Empty return Boolean;
end Transaction_Queue;
-------------------------------------------------------------------------
-- A task is needed for the Ticker_Tape because it has independent cyclic
-- activity. The Specialist and the Investor are best modeled with tasks
-- since they perform different actions simultaneously, and should be
-- asynchronous threads of control.
task Ticker_Tape;
task Specialist is
entry Buy (Order : in Order_Type);
entry Sell (Order : in Order_Type);
end Specialist;
task Investor;
-------------------------------------------------------------------------
task body Ticker_Tape is
...
begin
loop
Display.Shift_Tape_Left;
if not More_To_Send (Current_Tape_String) and then
not Transaction_Queue.Is_Empty
then
Transaction_Queue.Get (Current_Tape_Transaction);
... -- convert Transaction to string
end if;
if More_To_Send (Current_Tape_String) then
Display.Put_Character_On_Tape (Next_Char);
end if;
delay until Time_To_Shift_Tape;
Time_To_Shift_Tape := Time_To_Shift_Tape + Shift_Interval;
end loop;
end Ticker_Tape;
task body Specialist is
...
loop
select
accept Buy (Order : in Order_Type) do
...
end Buy;
...
or
accept Sell (Order : in Order_Type) do
...
end Sell;
...
else
-- match orders
...
Transaction_Queue.Put (New_Transaction);
...
end select;
end loop;
end Specialist;
task body Investor is
...
begin
loop
-- some algorithm that determines whether the investor
-- buys or sells, quantity, price, etc
...
if ... then
Specialist.Buy (Order);
end if;
if ... then
Specialist.Sell (Order);
end if;
end loop;
end Investor;
实现大型矩阵乘法算法分解的多个任务是多处理器目标环境中实现真实并发的机会示例。在单处理器目标环境中,由于上下文切换和共享系统资源带来的开销,这种方法可能不合理。
每隔 30 毫秒更新雷达显示的任务是任务支持循环活动的一个示例。
检测核反应堆过温状况并执行系统紧急关闭的任务是支持高优先级活动的任务示例。
这些指南反映了任务的预期用途。它们都围绕着这样一个事实展开:一个任务拥有自己的控制线程,该线程独立于分区的主子程序(或环境任务)。任务的概念模型是一个具有自己的虚拟处理器的独立程序。这提供了根据更接近这些实体的术语对问题域中的实体进行建模的机会,以及将物理设备视为独立于应用程序主算法的独立关注点的机会。任务还允许自然并发活动,这些活动可以在可用时映射到分区中的多个处理器。
您应将任务用于单独的控制线程。当您同步任务时,您应仅在尝试同步实际进程时使用 rendezvous 机制(例如,指定时间敏感的排序关系或紧密耦合的进程间通信)。但是,对于大多数同步需求,您应使用受保护对象(请参阅指南 6.1.1),受保护对象更灵活,可以最大限度地减少不必要的瓶颈。此外,被动任务可能比主动任务更适合通过受保护对象进行建模。
多个任务之间共享的资源(如设备)需要控制和同步,因为它们的操作不是原子的。在显示屏上绘制一个圆圈可能需要执行许多低级操作,而不会被另一个任务中断。显示管理器将确保在所有这些操作完成之前,没有其他任务访问显示屏。
- 考虑使用鉴别式来最大限度地减少对显式初始化操作的需求(基本原理 1995,第 9.1 节)。
- 考虑使用鉴别式来控制受保护对象的复合组件,包括设置入口族的大小(基本原理 1995,第 9.1 节)。
- 考虑使用鉴别式来设置受保护对象的优先级(基本原理 1995,第 9.1 节)。
- 考虑使用鉴别式来识别对受保护对象的 interrupt(基本原理 1995,第 9.1 节)。
- 考虑使用带有鉴别式的任务类型来指示(基本原理 1995,第 9.6 节)
- 类型中各个任务的优先级、存储大小和入口族大小
- 与任务相关联的数据(通过访问鉴别式)
以下代码片段显示了如何使用带有鉴别式的任务类型将数据与任务相关联(基本原理 1995,第 9.6 节)
type Task_Data is
record
... -- data for task to work on
end record;
task type Worker (D : access Task_Data) is
...
end;
-- When you declare a task object of type Worker, you explicitly associate this task with
-- its data through the discriminant D
Data_for_Worker_X : aliased Task_Data := ...;
X : Worker (Data_for_Worker_X'Access);
以下示例显示了如何使用鉴别式将数据与任务相关联,从而允许在声明任务时对其进行参数化,并消除与任务进行初始 rendezvous 的需要
task type Producer (Channel : Channel_Number; ID : ID_Number);
task body Producer is
begin
loop
... -- generate an item
Buffer.Put (New_Item);
end loop;
end Producer;
...
Keyboard : Producer (Channel => Keyboard_Channel, ID => 1);
Mouse : Producer (Channel => Mouse_Channel, ID => 2);
下一个示例显示了如何使用初始 rendezvous 将数据与任务相关联。这比前面的示例更复杂,更容易出错。由于 Ada 95 中提供了带有任务类型和受保护类型的鉴别式,因此不再需要这种方法
task type Producer is
entry Initialize (Channel : in Channel_Number; ID : in ID_Number);
end Producer;
task body Producer is
IO_Channel : Channel_Number;
Producer_ID : ID_Number;
begin
accept Initialize (Channel : in Channel_Number; ID : in ID_Number) do
IO_Channel := Channel;
Producer_ID := ID;
end;
loop
... -- generate an item
Buffer.Put (New_Item);
end loop;
end Producer;
...
Keyboard : Producer;
Mouse : Producer;
...
begin
...
Keyboard.Initialize (Channel => Keyboard_Channel, ID => 1);
Mouse.Initialize (Channel => Mouse_Channel, ID => 2);
...
使用辨别式参数化受保护对象提供了一种低开销的方式来专门化受保护对象。您无需声明和调用专门的子程序来将此信息传递给受保护对象。
任务辨别式提供了一种方法来识别或参数化任务,而无需初始会合的开销。例如,您可以使用此辨别式来初始化任务或告诉它它是谁(在任务数组中)。更重要的是,您可以将辨别式与特定数据相关联。当使用访问辨别式时,您可以将数据安全地绑定到任务,因为访问辨别式是常量,不能从任务中分离出来(Rationale 1995,§9.6)。这减少了并可能消除任务并行激活中的瓶颈(Rationale 1995,§9.6)。
使用访问辨别式来初始化任务存在一个潜在的危险,即引用的数据可能会在会合后发生更改。应该考虑这种可能性及其影响,并在必要时采取适当的措施(例如,复制引用的数据,并且在初始化后不要依赖辨别式指向的数据)。
- 考虑使用单个任务声明来声明并发任务的唯一实例。
- 考虑使用单个受保护声明来声明受保护对象的唯一实例。
以下示例说明了此处讨论的任务和受保护对象类型的语法差异。Buffer 是静态的,但其类型是匿名的。没有声明类型名称来使您能够声明相同类型的其他对象。
task Buffer;
由于显式声明,任务类型 Buffer_Manager 不是匿名的。Channel 是静态的并且有名称,其类型不是匿名的。
task type Buffer_Manager;
Channel : Buffer_Manager;
使用匿名任务和匿名类型的受保护对象避免了大量仅使用一次的任务和受保护类型,并且这种做法向维护人员传达了没有其他相同类型的任务或受保护对象。如果以后需要添加相同类型的其他任务或受保护对象,则将匿名任务转换为任务类型或将匿名受保护对象转换为受保护类型的所需工作量很小。
当需要时,一致且合乎逻辑地使用任务和受保护类型有助于理解。可以使用公共任务类型声明相同任务。可以使用公共受保护类型声明相同的受保护对象。动态分配的任务或受保护结构是在必须动态创建和销毁任务或受保护对象,或者必须通过不同的名称引用它们时必要的。
虽然将任务或受保护对象从匿名类型更改为声明类型很简单,但软件架构的结构性更改可能并非易事。引入多个声明类型的任务或受保护对象可能需要更改类型的范围,并且可能会更改同步任务和受保护对象网络的行为。
- 由于潜在的高启动开销,请最小化任务的动态创建;通过让任务在某些适当的入口队列中等待新工作来重用任务。
以下示例中使用的方法不建议使用。该示例显示了动态分配的任务和受保护对象需要谨慎的原因。它说明了如何将动态任务与其名称分离。
task type Radar_Track;
type Radar_Track_Pointer is access Radar_Track;
Current_Track : Radar_Track_Pointer;
---------------------------------------------------------------------
task body Radar_Track is
begin
loop
-- update tracking information
...
-- exit when out of range
delay 1.0;
end loop;
...
end Radar_Track;
---------------------------------------------------------------------
...
loop
...
-- Radar_Track tasks created in previous passes through the loop
-- cannot be accessed from Current_Track after it is updated.
-- Unless some code deals with non-null values of Current_Track,
-- (such as an array of existing tasks)
-- this assignment leaves the existing Radar_Track task running with
-- no way to signal it to abort or to instruct the system to
-- reclaim its resources.
Current_Track := new Radar_Track;
...
end loop;
在许多实现中,启动任务会产生很大的开销。如果应用程序需要动态创建的任务,则应该使用顶层循环来实现任务,以便任务在完成给定工作后,可以循环回来并等待新工作。
当需要允许任务和受保护对象的数量在执行期间变化时,可以使用动态分配的任务和受保护对象。当必须确保任务以特定顺序激活时,应该使用动态分配的任务,因为 Ada 语言没有为静态分配的任务对象定义激活顺序。在使用动态分配的任务和受保护对象时,您会面临与使用堆相同的挑战。
- 除非您的编译器支持实时附件(Ada 参考手册 1995,附件 D)和优先级调度,否则不要依赖 pragma Priority。
- 通过使用受保护对象和最高优先级来最小化优先级反转的风险。
- 不要依赖任务优先级来实现特定任务执行顺序。
例如,让任务具有以下优先级
task T1 is
pragma Priority (High);
end T1;
task T2 is
pragma Priority (Medium);
end T2;
task Server is
entry Operation (...);
end Server;
----------------------------
task body T1 is
begin
...
Server.Operation (...);
...
end T1;
task body T2 is
begin
...
Server.Operation (...);
...
end T2;
task body Server is
begin
...
accept Operation (...);
...
end Server;
在执行的某个时刻,T1 被阻塞。否则,T2 和 Server 可能永远不会执行。如果 T1 被阻塞,T2 可能在 T1 之前到达其对 Server 的入口(Operation)的调用。假设这种情况已经发生,并且 T1 现在在其入口调用之前,Server 没有机会接受 T2 的调用。
这是迄今为止事件的时间线
T1 阻塞 T2 调用 Server.Operation T1 解阻塞 T1 调用 Server.Operation - Server 接受来自 T1 的调用还是来自 T2 的调用?
您可能期望,由于其更高的优先级,T1 的调用将在 Server 接受 T2 的调用之前被 Server 接受。但是,入口调用按先入先出 (FIFO) 顺序排队,而不是按优先级排队(除非使用 pragma Queueing_Policy)。因此,T1 和 Server 之间的同步不受 T1 优先级的影响。因此,来自 T2 的调用首先被接受。这是一种优先级反转。(附件 D 可以更改 FIFO 队列的默认策略。)
解决方案可能是为高优先级用户提供一个入口,为中优先级用户提供一个入口。
---------------------------------------------------------------------
task Server is
entry Operation_High_Priority;
entry Operation_Medium_Priority;
...
end Server;
---------------------------------------------------------------------
task body Server is
begin
loop
select
accept Operation_High_Priority do
Operation;
end Operation_High_Priority;
else -- accept any priority
select
accept Operation_High_Priority do
Operation;
end Operation_High_Priority;
or
accept Operation_Medium_Priority do
Operation;
end Operation_Medium_Priority;
or
terminate;
end select;
end select;
end loop;
...
end Server;
---------------------------------------------------------------------
但是,在这种方法中,T1 仍然在 T2 已经获得 Server 任务的控制权时等待 Operation 的一次执行。此外,这种方法会增加通信复杂性(参见指南 6.2.6)。
pragma Priority 允许为任务设置相对优先级以完成调度。对于硬截止期限调度,精度成为一个关键问题。但是,使用优先级会带来一些问题,需要谨慎。
优先级反转是指当低优先级任务获得服务而高优先级任务仍然被阻塞时发生的现象。在第一个示例中,这是因为入口队列按 FIFO 顺序服务,而不是按优先级服务。还有一种情况称为竞争条件。像第一个示例中的程序一样,程序通常可以按预期工作,只要 T1 只在 T2 未使用 Server.Operation 或未等待时才调用 Server.Operation。您不能依赖 T1 总是赢得比赛,因为这种行为更多地归因于命运而不是编程的优先级。当向不相关任务添加代码或将此代码移植到新目标时,竞争条件会发生变化。
您不应该依赖任务优先级来实现确切的执行顺序,也不应该依赖它们来实现互斥。虽然底层调度模型对于所有 Ada 95 实现来说都是通用的,但任务和受保护对象的调度、排队和锁定策略可能存在差异。所有这些因素都可能导致不同的执行顺序。如果需要确保执行顺序,则应该使用 Ada 的同步机制,即受保护对象或会合。
正在努力减少这些问题,包括引入一种称为优先级上限协议(Goodenough 和 Sha 1988)的调度算法。优先级上限协议将导致优先级反转的阻塞时间减少到仅一个临界区域(由任务中的条目定义)。该协议还通过为访问资源的每个任务提供一个上限优先级(该优先级与任何访问该资源的任务的优先级一样高)来消除死锁(除非任务递归地尝试访问临界区域)。该协议基于优先级继承,因此偏离了标准的 Ada 任务范式,该范式支持优先级上限模拟而不是优先级继承中发生的优先级上限阻塞。
优先级用于控制任务相对于彼此的运行时间。当两个任务都不在入口处阻塞等待时,将优先执行最高优先级任务。但是,应用程序中最关键的任务并不总是具有最高优先级。例如,支持任务或周期很短的任务可能具有更高的优先级,因为它们需要频繁运行。
所有经过生产质量验证的 Ada 95 编译器可能会支持 pragma Priority。但是,除非(附录 D 专门支持,否则应谨慎使用。
目前还没有关于如何将速率单调调度 (RMS) 的基本原则应用于 Ada 95 并发模型的普遍共识。RMS 的一个基本原则是安排所有周期性任务,以便周期较短的任务比周期较长的任务具有更高的优先级。但是,对于 Ada 95,提高任务的优先级(这些任务的工作突然变得关键)可能比等待执行任务重新调度它们更快。在这种情况下,可以使用带有 pragma Locking_Policy(Ceiling_Locking) 的保护对象作为服务器来最小化优先级反转,而不是使用任务。
延迟语句
[edit | edit source]指南
[edit | edit source]- 不要依赖于特定延迟的可实现性(Nissen 和 Wallis 1984)。
- 使用延迟直到而不是延迟语句,延迟到达到特定时间为止。
- 避免使用繁忙等待循环而不是延迟。
示例
[edit | edit source]周期性任务的相位是从指定参考点开始测量的完整周期经过的时间的比例。在以下示例中,不准确的延迟会导致周期性任务的相位随时间漂移(即,任务在周期中开始越来越晚)
周期性
loop
delay Interval;
...
end loop Periodic;
为了避免不准确的延迟漂移,您应该使用延迟直到语句。以下示例(Rationale 1995,§9.3)展示了如何使用平均周期满足周期性要求
task body Poll_Device is
use type Ada.Real_Time.Time;
use type Ada.Real_Time.Time_Span;
Poll_Time : Ada.Real_Time.Time := ...; -- time to start polling
Period : constant Ada.Real_Time.Time_Span := Ada.Real_Time.Milliseconds (10);
begin
loop
delay until Poll_Time;
... -- Poll the device
Poll_Time := Poll_Time + Period;
end loop;
end Poll_Device;
基本原理
[edit | edit source]延迟语句有两种形式。延迟将导致至少延迟指定的时间间隔。延迟直到导致延迟到绝对唤醒时间为止。您应该选择适合您的应用程序的形式。
Ada 语言定义只保证延迟时间是最小的。延迟或延迟直到语句的含义是任务在时间间隔过期之前不会被调度执行。换句话说,任务在时间过去后立即有资格恢复执行。但是,没有保证它在该时间之后何时(或是否)被调度,因为该任务所需的资源可能在延迟到期时不可用。
繁忙等待会干扰其他任务的处理。它会消耗完成它正在等待的活动的必需的处理器资源。即使是带有延迟的循环,如果计划的等待时间明显长于延迟间隔,也会产生繁忙等待的影响。如果任务无事可做,它应该在接受或选择语句、条目调用或适当的延迟处被阻塞。
相对延迟的到期时间向上舍入到最接近的时钟节拍。如果您使用 (附录 D 提供的实时时钟功能,但是,时钟节拍保证不超过 1 毫秒 (Ada 参考手册 1995,§D.8 [Annotated])。
笔记
[edit | edit source]您需要确保计算 Poll_Time := Poll_Time + Period; 的算术精度以避免漂移。
可扩展性和并发结构
[edit | edit source]指南
[edit | edit source]- 仔细考虑在带标记类型继承层次结构中放置保护类型组件的位置。
- 考虑使用泛型来提供需要保护对象提供的限制的数据类型的可扩展性。
基本原理
[edit | edit source]一旦将保护类型的组件添加到抽象数据类型的继承层次结构中,就会削弱该数据类型的进一步可扩展性。当您约束类型的并发行为(即,引入保护类型组件)时,您将失去在后续派生中修改该行为的能力。因此,当需要抽象数据类型的版本来施加保护对象提供的限制时,通过在继承层次结构的叶子处添加保护对象来最大限度地利用重用机会。
可以使用抽象数据类型的泛型实现来最大限度地利用常用保护操作(例如,互斥读/写操作)的可重用性。这些泛型实现然后提供可以与特定于各个应用程序的数据类型实例化的模板。
笔记
[edit | edit source]您可以通过以下三种方式之一解决继承层次结构中的同步问题
- 您可以将根声明为一个有限的带标记类型,该类型具有属于保护类型的组件,并为带标记类型提供通过调用该组件的保护操作来工作的基本操作。
- 给定一个实现抽象数据类型的带标记类型(可能是从多个扩展中得到的),您可以声明一个具有属于该带标记类型的组件的保护类型。然后,每个保护操作的主体将调用抽象数据类型的对应操作。保护操作提供互斥。
- 您可以使用混合方法,其中您声明一个具有某些带标记类型的组件的保护类型。然后,您可以使用此保护类型来实现一个新的根带标记类型(不是原始带标记类型的后代)。
通信
[edit | edit source]任务需要通信,这产生了使并发编程如此困难的大多数问题。如果使用得当,Ada 的任务间通信功能可以提高并发程序的可靠性;如果使用不当,它们可能会引入难以检测和纠正的细微错误。
高效的任务通信
[edit | edit source]指南
[edit | edit source]- 最小化在会合期间执行的工作。
- 最小化在任务的选择性接受循环中执行的工作。
- 考虑使用保护对象进行数据同步和通信。
示例
[edit | edit source]在以下示例中,接受体中的语句作为调用者任务和包含 Operation 和 Operation2 的任务 Server 执行的一部分执行。接受体后的语句在 Server 能够接受对 Operation 或 Operation2 的其他调用之前执行。
...
loop
select
accept Operation do
-- These statements are executed during rendezvous.
-- Both caller and server are blocked during this time.
...
end Operation;
...
-- These statements are not executed during rendezvous.
-- The execution of these statements increases the time required
-- to get back to the accept and might be a candidate for another task.
or
accept Operation_2 do
-- These statements are executed during rendezvous.
-- Both caller and server are blocked during this time.
...
end Operation_2;
end select;
-- These statements are also not executed during rendezvous,
-- The execution of these statements increases the time required
-- to get back to the accept and might be a candidate for another task.
end loop;
基本原理
[edit | edit source]为了最小化会合所需的时间,只有需要在会合期间执行的工作(例如,保存或生成参数)应该被允许在接受体中。
当将工作从 accept 语句体中移出并放置在选择性 accept 循环中,额外的任务仍然可能挂起调用者任务。如果调用者任务在服务器任务完成额外工作之前再次调用 entry 操作,则调用者将延迟,直到服务器完成额外工作。如果潜在的延迟不可接受,并且额外工作不需要在调用者任务的下一次服务之前完成,那么额外工作可以形成一个新任务的基础,该任务不会阻塞调用者任务。
对受保护对象的访问比任务产生更少的执行开销,并且在数据同步和通信方面比 rendezvous 更高效。必须设计受保护操作以使其有界、短小且不具有潜在阻塞性。
笔记
[edit | edit source]在某些情况下,可以在任务中添加额外的功能。例如,控制通信设备的任务可能负责定期执行函数以确保设备正常运行。这种添加应该谨慎进行,意识到任务的响应时间可能会受到影响(参见上面的论点)。
在任务的 rendezvous 或选择性 accept 循环中最小化执行的工作量,只有在它导致调用者和被调用者之间处理的额外重叠,或者由于执行时间缩短而可以调度其他任务时才能提高执行速度。因此,在多处理器环境中,执行速度的提高将最大。在单处理器环境中,执行速度的提高不会那么明显,甚至可能会有少量净损失。但是,如果应用程序将来可能移植到多处理器环境中,则该准则仍然适用。
防御性任务通信
[edit | edit source]指南
[edit | edit source]- 每当无法避免选择性 accept 语句(其所有备选方案都可能关闭)时,请提供针对异常 Program_Error 的处理程序(Honeywell 1986)。
- 系统地使用针对 Tasking_Error 的处理程序。
- 做好在 rendezvous 期间处理异常的准备。
- 考虑使用 when others 异常处理程序。
示例
[edit | edit source]此代码块允许从在尝试向另一个任务通信命令时引发的异常中恢复
Accelerate:
begin
Throttle.Increase(Step);
exception
when Tasking_Error => ...
when Constraint_Error => ...
when Throttle_Too_Wide => ...
...
end Accelerate;
在此 select 语句中,如果所有保护都恰好关闭,程序可以通过执行 else 部分继续执行。无需为 Program_Error 提供处理程序。其他异常仍然可能在评估保护或尝试通信时引发。您还需要在任务 Throttle 中包含一个异常处理程序,以便在 rendezvous 期间引发异常后它可以继续执行
...
Guarded:
begin
select
when Condition_1 =>
accept Entry_1;
or
when Condition_2 =>
accept Entry_2;
else -- all alternatives closed
...
end select;
exception
when Constraint_Error =>
...
end Guarded;
在此 select 语句中,如果所有保护都恰好关闭,将引发异常 Program_Error。其他异常仍然可能在评估保护或尝试通信时引发
Guarded:
begin
select
when Condition_1 =>
accept Entry_1;
or
when Condition_2 =>
delay Fraction_Of_A_Second;
end select;
exception
when Program_Error => ...
when Constraint_Error => ...
end Guarded;
...
基本原理
[edit | edit source]如果选择性 accept 语句(包含 accept 语句的 select 语句)被执行,而所有备选方案都已关闭(即,保护评估为 False 且没有不带保护的备选方案),则将引发异常 Program_Error,除非存在 else 部分。当所有备选方案都关闭时,任务将永远无法再执行,因此,其编程中肯定存在错误。必须做好处理此错误的准备,以防它发生。
由于 else 部分不能有保护,因此它永远不会被关闭为备选操作;因此,它的存在可以防止 Program_Error。但是,else 部分、延迟备选方案和终止备选方案是相互排斥的,因此您无法始终提供 else 部分。在这种情况下,您必须做好处理 Program_Error 的准备。
每当调用任务尝试通信时,都可能在调用任务中引发异常 Tasking_Error。有很多情况允许这样做。调用任务无法阻止其中很少一部分。
如果在 rendezvous 期间引发异常,并且在 accept 语句中没有处理,它将传播到两个任务,并且必须在两个地方进行处理(参见准则 5.8)。可以使用 others 异常的处理来避免将意外异常传播给调用者(当这是期望的效果时)以及在 rendezvous 中本地化处理意外异常的逻辑。处理完后,通常应该再次引发未知异常,因为可能需要在任务体的最外层范围内做出如何处理它的最终决定。
笔记
[edit | edit source]还有其他方法可以防止在选择性 accept 中发生 Program_Error。这些方法涉及至少保留一个备选方案不受保护,或者证明至少一个保护在所有情况下都会评估为 True。这里要强调的是,您或您的继任者在尝试这样做时会犯错误,因此您应该做好处理不可避免的异常的准备。
属性 'Count、'Callable 和 'Terminated
[edit | edit source]指南
[edit | edit source]- 不要依赖任务属性 'Callable 或 'Terminated 的值(Nissen 和 Wallis 1984)。
- 不要依赖属性来避免在 entry 调用上发生 Tasking_Error。
- 对于任务,不要依赖 entry 属性 'Count 的值。
- 与使用任务 entry 属性 'Count 相比,使用受保护 entry 属性 'Count 更可靠。
示例
[edit | edit source]在以下示例中,Dispatch'Callable 是一个布尔表达式,表示是否可以对任务 Intercept 进行调用而不会引发异常 Tasking_Error。Dispatch'Count 表示当前在 entry Transmit 处等待的调用者数量。Dispatch'Terminated 是一个布尔表达式,表示任务 Dispatch 是否处于终止状态。
此任务编程不当,因为它依赖于 'Count 属性的值在评估和执行它们之间不会改变
---------------------------------------------------------------------
task body Dispatch is
...
select
when Transmit'Count > 0 and Receive'Count = 0 =>
accept Transmit;
...
or
accept Receive;
...
end select;
...
end Dispatch;
---------------------------------------------------------------------
如果在评估条件和启动调用之间抢占了以下代码,则任务仍然可调用的假设可能不再有效
...
if Dispatch'Callable then
Dispatch.Receive;
end if;
...
基本原理
[edit | edit source]属性 'Callable、'Terminated 和 'Count 都容易受到竞争条件的影响。在您引用属性和您采取行动之间,属性的值可能会发生变化。属性 'Callable 和 'Terminated 在分别变为 False 和 True 后会传达可靠的信息。如果 'Callable 为 False,则可以预期可调用状态保持不变。如果 'Terminated 为 True,则可以预期任务保持终止状态。否则,'Terminated 和 'Callable 可能会在您的代码测试它们的时间和响应结果的时间之间发生变化。
Ada 参考手册 1995,第 9.9 节 [带注释的] 本身警告了 'Count 值的异步增加和减少。任务可以从 entry 队列中删除,原因是 abort 语句的执行以及定时 entry 调用的超时。在选择性 accept 语句的保护中使用此属性可能会导致打开不应该在 'Count 值发生变化的情况下打开的备选方案。
属性 'Count 的值对于受保护单元是稳定的,因为对 entry 队列的任何更改本身都是一个受保护的操作,在任何其他受保护的操作正在进行时都不会发生。但是,当在受保护单元的 entry 障碍中使用 'Count 时,您应该记住,在排队给定调用者之前和之后都会评估障碍条件。
不受保护的共享变量
[edit | edit source]- 使用受保护子程序或入口的调用在任务之间传递数据,而不是使用不受保护的共享变量。
- 不要使用不受保护的共享变量作为任务同步设备。
- 不要在保护条件中引用非局部变量。
- 如果需要不受保护的共享变量,请使用 pragma Volatile 或 Atomic。
这段代码要么多次打印同一行,要么无法打印某些行,要么以不确定的方式打印乱码行(一行的一部分后面跟着另一行的一部分)。这是因为读取命令的任务和对命令进行操作的任务之间没有同步或互斥。在不知道它们的相对调度的情况下,无法预测实际结果。
-----------------------------------------------------------------------
task body Line_Printer_Driver is
...
begin
loop
Current_Line := Line_Buffer;
-- send to device
end loop;
end Line_Printer_Driver;
-----------------------------------------------------------------------
task body Spool_Server is
...
begin
loop
Disk_Read (Spool_File, Line_Buffer);
end loop;
end Spool_Server;
-----------------------------------------------------------------------
以下示例展示了一个自动售货机,它将请求的金额分配到一个适当大小的容器中。保护条件引用全局变量 Num_Requested 和 Item_Count,这可能导致分配到不合适大小容器中的金额不正确。
Num_Requested : Natural;
Item_Count : Natural := 1000;
task type Request_Manager (Personal_Limit : Natural := 1) is
entry Make_Request (Num : Natural);
entry Get_Container;
entry Dispense;
end Request_Manager;
task body Request_Manager is
begin
loop
select
accept Make_Request (Num : Natural) do
Num_Requested := Num;
end Make_Request;
or
when Num_Requested < Item_Count =>
accept Get_Container;
...
or
when Num_Requested < Item_Count =>
accept Dispense do
if Num_Requested <= Personal_Limit then
Ada.Text_IO.Put_Line ("Please pick up items.");
else
Ada.Text_IO.Put_Line ("Sorry! Requesting too many items.");
end if;
end Dispense;
end select;
end loop;
end Request_Manager;
R1 : Request_Manager (Personal_Limit => 10);
R2 : Request_Manager (Personal_Limit => 2);
R1 和 R2 执行的交错会导致 Num_Requested 在接受对 Dispense 的入口调用之前被更改。因此,R1 可能会收到比请求更少的项目,或者 R2 的请求可能会被拒绝,因为请求管理器认为 R2 请求的项目超过了 R2 的个人限制。通过使用局部变量,您将分配正确的数量。此外,通过使用 pragma Volatile(Ada 参考手册 1995,第 C.6 节 [注释]),您可以确保在评估保护条件时重新评估 Item_Count。鉴于变量 Item_Count 在此任务体中没有更新,否则编译器可能会优化代码,并且不会生成代码来每次读取时重新评估 Item_Count。
Item_Count : Natural := 1000;
pragma Volatile (Item_Count);
task body Request_Manager is
Local_Num_Requested : Natural := 0;
begin
loop
select
accept Make_Request (Num : Natural) do
Local_Num_Requested := Num;
end Make_Request;
or
when Local_Num_Requested <= Personal_Limit =>
accept Get_Container;
...
or
when Local_Num_Requested < Item_Count =>
accept Dispense do
... -- output appropriate message if couldn't service request
end Dispense;
Item_Count := Item_Count - Local_Num_Requested;
end select;
end loop;
end Request_Manager;
有许多技术用于保护和同步数据访问。您必须自己编程大多数技术才能使用它们。编写共享不受保护数据的程序很难。如果操作不正确,程序的可靠性会受到影响。
Ada 提供受保护对象,这些对象封装并提供对任务之间共享的受保护数据的同步访问。预计受保护对象将提供比通常需要引入额外任务来管理共享数据的 rendezvous 更好的性能。使用不受保护的共享变量比受保护对象或 rendezvous 更容易出错,因为程序员必须确保不受保护的共享变量是独立寻址的,并且读取或更新同一不受保护的共享变量的操作是顺序的(Ada 参考手册 1995,第 9.0 节 [注释];理由 1995,第 II.9 节)。
上面的第一个示例存在竞争条件,需要完美的执行交错。通过引入一个由 Spool_Server 设置并由 Line_Printer_Driver 重置的标志,可以使这段代码更可靠。在每个任务循环中添加 if (condition flag) then delay ... else,以确保满足交错条件。但是,请注意,这种方法需要延迟和相关的重新调度。据推测,这种重新调度开销正是通过不使用 rendezvous 来避免的。
您可能需要使用共享内存中的对象来在以下之间进行数据通信(理由 1995,第 C.5 节)
- Ada 任务
- Ada 程序和并发非 Ada 进程
- Ada 程序和硬件设备
如果您的环境支持系统编程附录(Ada 参考手册 1995,附录 C),则您应该指定对共享对象的加载和存储是否必须是不可分割的。如果指定 pragma Atomic,请确保该对象满足底层硬件对大小和对齐的要求。多个共享预定义随机数生成器和某些输入/输出子程序的任务可能会导致对共享状态的保护更新出现问题。Ada 参考手册 1995,第 A.5.2 节 [注释] 指出任务需要同步它们对随机数生成器(包 Ada.Numerics.Float_Random 和 Ada.Numerics.Discrete_Random)的访问。有关 I/O 问题,请参见指南 7.7.5。
- 对任务入口使用条件入口调用时要小心。
- 对带有 else 部分的选择性接受要小心。
- 不要依赖任务入口的定时入口调用中的特定延迟。
- 不要依赖带有延迟备选方案的选择性接受中的特定延迟。
- 考虑使用受保护对象来代替 rendezvous 用于面向数据的同步。
以下代码中的条件入口调用会导致潜在的竞争条件,该条件可能会退化为繁忙等待循环(即,一遍又一遍地执行相同的计算)。如果包含循环的任务(以下代码片段中所示)的优先级高于包含入口 Request_New_Coordinates 的任务 Current_Position,则该任务可能永远不会执行,因为它不会释放处理资源。
task body Calculate_Flightpath is
begin
...
loop
select
Current_Position.Request_New_Coordinates (X, Y);
-- calculate projected location based on new coordinates
...
else
-- calculate projected location based on last locations
...
end select;
end loop;
...
end Calculate_Flightpath;
如所示,添加延迟可能会允许 Current_Position 执行,直到它到达对 Request_New_Coordinates 的接受。
task body Calculate_Flightpath is
begin
...
loop
select
Current_Position.Request_New_Coordinates(X, Y);
-- calculate projected location based on new coordinates
...
else
-- calculate projected location based on last locations
...
delay until Time_To_Execute;
Time_To_Execute := Time_To_Execute + Period;
end select;
end loop;
...
end Calculate_Flightpath;
以下带有 else 的选择性接受不会退化为繁忙等待循环,仅仅是因为添加了延迟语句。
task body Buffer_Messages is
...
begin
...
loop
delay until Time_To_Execute;
select
accept Get_New_Message (Message : in String) do
-- copy message to parameters
...
end Get_New_Message;
else -- Don't wait for rendezvous
-- perform built in test Functions
...
end select;
Time_To_Execute := Time_To_Execute + Period;
end loop;
...
end Buffer_Messages;
如果与反应堆的通信丢失超过 25 毫秒会导致严重情况,则以下定时入口调用可能被认为是不可接受的实现。
task body Monitor_Reactor is
...
begin
...
loop
select
Reactor.Status(OK);
or
delay 0.025;
-- lost communication for more that 25 milliseconds
Emergency_Shutdown;
end select;
-- process reactor status
...
end loop;
...
end Monitor_Reactor;
在以下“带有延迟的选择性接受”示例中,坐标计算函数的精度受时间限制。例如,除非 Period 在 + 或 - 0.005 秒内,否则无法获得所需的精度。由于延迟语句的不准确性,无法保证此周期。
task body Current_Position is
begin
...
loop
select
accept Request_New_Coordinates (X : out Integer;
Y : out Integer) do
-- copy coordinates to parameters
...
end Request_New_Coordinates;
or
delay until Time_To_Execute;
end select;
Time_To_Execute := Time_To_Execute + Period;
-- Read Sensors
-- execute coordinate transformations
end loop;
...
end Current_Position;
使用这些结构始终存在竞争条件的风险。在循环中使用它们,尤其是在任务优先级选择不佳的情况下,可能会导致繁忙等待。
这些结构在很大程度上取决于实现。对于条件入口调用和带有 else 部分的选择性接受,Ada 参考手册 1995,第 9.7 节 [注释] 没有定义“立即”。对于定时入口调用和带有延迟备选方案的选择性接受,实现者可能对时间有不同的理解,这些理解彼此不同,也与您自己的理解不同。与延迟语句类似,select 结构上的延迟备选方案的等待时间可能比所需时间更长(参见指南 6.1.7)。
受保护对象为提供面向数据的同步提供了一种有效的方式。对受保护对象的执行操作比任务产生的执行开销更小,并且对于数据同步和通信而言,比 rendezvous 更有效。有关受保护对象的这种用法的示例,请参见指南 6.1.1。
- 最大限度地减少每个任务的 accept 和 select 语句的数量。
- 最大限度地减少每个入口的 accept 语句的数量。
使用
accept A;
if Mode_1 then
-- do one thing
else -- Mode_2
-- do something different
end if;
而不是
if Mode_1 then
accept A;
-- do one thing
else -- Mode_2
accept A;
-- do something different
end if;
本指南旨在降低概念复杂性。仅应引入理解外部可观察任务行为所需的条目。如果存在多个不同的接受和选择语句,而它们不会以对任务用户重要的方式修改任务行为,那么选择/接受语句的泛滥就会引入不必要的复杂性。对任务用户重要的外部可观察行为包括任务计时行为、由条目调用触发的任务会合、条目的优先级或数据更新(其中数据在任务之间共享)。
Sanden (1994) 认为,您需要权衡与 accept 语句关联的守卫的复杂性与选择/接受语句的数量。Sanden (1994) 展示了银行出纳员队列控制器的示例,该控制器具有两种模式:打开和关闭。您可以使用一个循环和两个选择语句来实现这种情况,一个用于打开模式,另一个用于关闭模式。尽管您使用更多选择/接受语句,但 Sanden (1994) 认为,生成的程序更易于理解和验证。
任务使用 Ada 的任务间通信功能相互交互的能力使得以纪律化的方式管理计划内或计划外(例如,响应灾难性异常条件)终止变得尤为重要。否则,由于单个任务的终止,可能会导致大量不受欢迎且不可预测的副作用。关于终止的指南侧重于任务的终止。只要可能,您应该使用受保护的对象(参见指南 6.1.1),从而避免与任务相关的终止问题。
- 考虑在每个任务内的主循环中为会合使用异常处理程序。
在以下示例中,使用主传感器引发的异常用于将模式更改为降级模式,仍然允许系统执行。
...
loop
Recognize_Degraded_Mode:
begin
case Mode is
when Primary =>
select
Current_Position_Primary.Request_New_Coordinates (X, Y);
or
delay 0.25;
-- Decide whether to switch modes;
end select;
when Degraded =>
Current_Position_Backup.Request_New_Coordinates (X, Y);
end case;
...
exception
when Tasking_Error | Program_Error =>
Mode := Degraded;
end Recognize_Degraded_Mode;
end loop;
...
允许任务终止可能不支持系统的要求。如果主任务循环内的会合没有异常处理程序,任务的功能可能无法执行。
使用异常处理程序是保证从对异常任务的条目调用中恢复的唯一方法。在进行条目调用之前使用 'Terminated 属性测试任务的可用性可能会引入竞态条件,在这种情况下,被测试任务在测试之后但在条目调用之前失败(参见指南 6.2.3)。
- 不要无意中创建非终止任务。
- 显式关闭依赖于库包的任务。
- 确认任务在使用 Ada.Unchecked_Deallocation 释放之前已终止。
- 考虑使用带终止备选方案的选择语句,而不是单独使用接受语句。
- 考虑为每个不需要 else 部分或 delay 的选择性接受提供终止备选方案。
- 在环境任务完成等待其他任务之后,不要在用户定义的 Finalize 过程中声明或创建任务。
此任务将永远不会终止。
---------------------------------------------------------------------
task body Message_Buffer is
...
begin -- Message_Buffer
loop
select
when Head /= Tail => -- Circular buffer not empty
accept Retrieve (Value : out Element) do
...
end Retrieve;
or
when not ((Head = Index'First and then
Tail = Index'Last) or else
(Head /= Index'First and then
Tail = Index'Pred(Head)) )
=> -- Circular buffer not full
accept Store (Value : in Element);
end select;
end loop;
...
end Message_Buffer;
---------------------------------------------------------------------
隐式环境任务在所有其他任务终止之前不会终止。环境任务充当作为分区执行的一部分创建的所有其他任务的主任务;它等待所有此类任务的终止以执行任何剩余分区的对象的最终化。因此,分区将存在,直到所有库任务终止。
非终止任务是指其主体包含非终止循环且没有带终止的备选方案的选择性接受,或者依赖于库包的任务。包含任务的子程序或块的执行在任务终止之前无法完成。任何调用包含非终止任务的子程序的任务都将被无限期延迟。
依赖于库包的任务不能使用带备选方案的选择性接受结构强制终止,而应在程序关闭期间显式终止。显式关闭依赖于库包的任务的一种方法是为它们提供退出条目,并让主子程序在终止之前调用退出条目。
在 Ada 参考手册 1995,§13.1.2[带注释] 中指出,释放受限制的未终止任务对象会导致出现边界错误。危险在于由于释放任务对象而导致的鉴别器释放。程序执行结束时未终止任务包含边界错误的影响是未定义的。
如果没有任何任务调用与该语句关联的条目,则执行 accept 语句或没有 else 部分、延迟或终止备选方案的选择性 accept 语句将无法继续。这会导致死锁。遵循为每个没有 else 或延迟的选择性接受提供终止备选方案的指南需要在任务主体中编程多个终止点。读者可以轻松地“知道在哪里寻找”任务主体中的正常终止点。终止点是主体语句序列的结尾和选择语句的备选方案。
环境任务正常或异常终止后,语言没有指定是否要等待在分区中受控对象的最终化期间激活的任务。环境任务正在等待分区中的所有其他任务完成时,在最终化期间启动新任务会导致出现边界错误(Ada 参考手册 1995,§10.2[带注释])。在创建或激活此类任务期间可能会引发 Program_Error 异常。
如果您正在实现循环执行程序,您可能需要一个不会终止的调度任务。有人说,任何实时系统都不应该被编程为终止。这是极端的。许多实时系统的系统关闭是一个理想的安全功能。
如果您正在考虑编写一个永不终止的任务,请确保它不依赖于调用者会期望返回的任务块或子程序。由于整个程序都可以作为重用候选(参见第 8 章),请注意该任务(以及它所依赖的任何内容)将不会终止。还要确保,对于您希望终止的任何其他任务,其终止不应等待此任务的终止。请重新阅读并充分理解 1995 年 Ada 参考手册,第 9.3 节 [带注释的] 关于“任务依赖性 - 任务的终止”。
Abort 语句
[edit | edit source]指南
[edit | edit source]- 避免使用 abort 语句。
- 考虑使用异步选择语句而不是 abort 语句。
- 尽量减少异步选择语句的使用。
- 避免从任务或异步选择语句的可中止部分分配非原子全局对象。
示例
[edit | edit source]如果应用程序需要,请提供一个任务入口用于有序关机。
以下异步控制转移的示例显示了一个数据库事务。除非提交事务已开始,否则数据库操作可能会被取消(通过特殊的输入键)。该代码摘自理据(1995 年,第 9.4 节)
with Ada.Finalization;
package Txn_Pkg is
type Txn_Status is (Incomplete, Failed, Succeeded);
type Transaction is new Ada.Finalization.Limited_Controlled with private;
procedure Finalize (Txn : in out transaction);
procedure Set_Status (Txn : in out Transaction;
Status : in Txn_Status);
private
type Transaction is new Ada.Finalization.Limited_Controlled with
record
Status : Txn_Status := Incomplete;
pragma Atomic (Status);
. . . -- More components
end record;
end Txn_Pkg;
-----------------------------------------------------------------------------
package body Txn_Pkg is
procedure Finalize (Txn : in out Transaction) is
begin
-- Finalization runs with abort and ATC deferred
if Txn.Status = Succeeded then
Commit (Txn);
else
Rollback (Txn);
end if;
end Finalize;
. . . -- body of procedure Set_Status
end Txn_Pkg;
----------------------------------------------------------------------------
-- sample code block showing how Txn_Pkg could be used:
declare
Database_Txn : Transaction;
-- declare a transaction, will commit or abort during finalization
begin
select -- wait for a cancel key from the input device
Input_Device.Wait_For_Cancel;
-- the Status remains Incomplete, so that the transaction will not commit
then abort -- do the transaction
begin
Read (Database_Txn, . . .);
Write (Database_Txn, . . .);
. . .
Set_Status (Database_Txn, Succeeded);
-- set status to ensure the transaction is committed
exception
when others =>
Ada.Text_IO.Put_Line ("Operation failed with unhandled exception:");
Set_Status (Database_Txn, Failed);
end;
end select;
-- Finalize on Database_Txn will be called here and, based on the recorded
-- status, will either commit or abort the transaction.
end;
基本原理
[edit | edit source]当执行 abort 语句时,无法知道目标任务之前在做什么。目标任务负责的数据可能处于不一致状态。以这种不受控制的方式中止任务对系统的影响需要仔细分析。系统设计必须确保所有依赖于中止任务的任务都能检测到终止并做出适当的响应。
任务直到到达中止完成点才会中止,例如开始或结束细化、延迟语句、接受语句、入口调用、选择语句、任务分配或执行异常处理程序。因此,abort 语句可能不会像您预期的那样立即释放处理器资源。它也可能不会停止一个失控的任务,因为该任务可能正在执行一个不包含任何中止完成点的无限循环。不能保证任务在多处理器系统中直到中止完成点才会中止,但任务几乎总是会立即停止运行。
异步选择语句允许外部事件导致任务从新点开始执行,而不必中止并重新启动任务(理据 1995 年,第 9.3 节)。由于触发语句和可中止语句并行执行,直到其中一个完成并迫使另一个被放弃,您只需要一个控制线程。异步选择语句提高了可维护性,因为可中止语句被清楚地限定,并且转移不会被错误地重定向。
在任务体和异步选择的可中止部分中,应避免分配非原子全局对象,主要是因为在非原子分配完成之前存在中止的风险。如果您在应用程序中有一个或多个 abort 语句,并且分配被打断,则目标对象可能会变得异常,并且随后使用该对象会导致错误执行 (1995 年 Ada 参考手册,第 9.8 节 [带注释的])。在标量对象的情况下,可以使用 'Valid 属性,但对于非标量对象没有等效的属性。(有关 'Valid 属性的讨论,请参见指南 5.9.1。)您仍然可以安全地分配局部对象并调用全局保护对象的运算。
异常终止
[edit | edit source]指南
[edit | edit source]- 在任务体的末尾放置一个 others 的异常处理程序。
- 考虑让每个任务体末尾的异常处理程序报告任务的死亡。
- 不要依赖任务状态来确定是否可以与任务进行 rendezvous。
示例
[edit | edit source]这是许多更新雷达屏幕上 blip 位置的任务之一。启动时,它会获得其父级用来识别它的名称的一部分。如果它因异常而终止,它会在其父级的其中一个数据结构中发出信号。
task type Track (My_Index : Track_Index) is
...
end Track;
---------------------------------------------------------------------
task body Track is
Neutral : Boolean := True;
begin -- Track
select
accept ...
...
or
terminate;
end select;
...
exception
when others =>
if not Neutral then
Station(My_Index).Status := Dead;
end if;
end Track;
---------------------------------------------------------------------
基本原理
[edit | edit source]如果任务内部发生异常,而它没有处理程序,则该任务将终止。在这种情况下,异常不会传播到任务之外(除非它发生在 rendezvous 期间)。该任务只会死亡,不会向程序中的其他任务发出通知。因此,在任务中提供异常处理程序,尤其是 others 的处理程序,可以确保任务在发生异常后可以重新获得控制。如果任务在处理异常后无法正常进行,则这将为其提供干净地关闭自身并通知负责因任务异常终止而导致的错误恢复的任务的机会。
你不应该使用任务状态来确定是否可以与任务进行 rendezvous。如果任务 A 依赖于任务 B,而任务 A 在与任务 B 进行 rendezvous 之前检查状态标志,则可能在状态测试和 rendezvous 之间任务 B 失败。在这种情况下,任务 A 必须提供一个异常处理程序来处理由调用异常任务的入口引发的 Tasking_Error 异常(参见指南 6.3.1)。
循环任务调用
[edit | edit source]指南
[edit | edit source]- 不要调用一个直接或间接导致调用原始调用任务的入口的任务入口。
基本原理
[edit | edit source]如果一个任务直接或间接通过一个循环调用链调用它自己的一个入口,则会导致一个称为任务死锁的软件故障。
设置退出状态
[edit | edit source]指南
[edit | edit source]- 在使用 Ada.Command_Line.Set_Exit_Status 过程时,避免在设置退出状态码时出现竞争条件。
- 在一个包含多个任务的程序中,封装、序列化和检查对 Ada.Command_Line.Set_Exit_Status 过程的调用。
基本原理
[edit | edit source]根据 Ada 的规则,库级包中的任务可能在主程序任务之后终止。如果程序允许多个任务使用 Set_Exit_Status,则不能保证任何特定状态值是实际返回的值。
总结
[edit | edit source]并发选项
[edit | edit source]- 考虑使用受保护的对象来提供对数据的互斥访问。
- 考虑使用受保护的对象来控制或同步对多个任务共享数据的访问。
- 考虑使用受保护的对象来实现同步,例如被动资源监视器。
- 考虑将受保护的对象封装在包的私有部分或主体中。
- 考虑使用受保护的过程来实现中断处理程序。
- 如果硬件中断的最大优先级高于分配给处理程序的优先级上限,则不要将受保护的处理程序附加到该硬件中断。
- 避免在入口屏障中使用全局变量。
- 避免使用带有副作用的屏障表达式。
- 使用任务来模拟问题域中选定的异步控制线程。
- 考虑使用任务来定义并发算法。
- 如果应用程序需要同步无缓冲通信,请考虑使用 rendezvous。
- 考虑使用鉴别式来最大限度地减少对显式初始化操作的需求(基本原理 1995,第 9.1 节)。
- 考虑使用鉴别式来控制受保护对象的复合组件,包括设置入口族的大小(基本原理 1995,第 9.1 节)。
- 考虑使用鉴别式来设置受保护对象的优先级(基本原理 1995,第 9.1 节)。
- 考虑使用鉴别式来识别对受保护对象的 interrupt(基本原理 1995,第 9.1 节)。
- 考虑使用带有鉴别式的任务类型来指示(基本原理 1995,第 9.6 节)
- 类型中各个任务的优先级、存储大小和入口族大小
- 与任务相关联的数据(通过访问鉴别式)
- 考虑使用单个任务声明来声明并发任务的唯一实例。
- 考虑使用单个受保护声明来声明受保护对象的唯一实例。
- 由于潜在的高启动开销,请最小化任务的动态创建;通过让任务在某些适当的入口队列中等待新工作来重用任务。
- 除非您的编译器支持实时附件(Ada 参考手册 1995,附件 D)和优先级调度,否则不要依赖 pragma Priority。
- 通过使用受保护对象和最高优先级来最小化优先级反转的风险。
- 不要依赖任务优先级来实现特定任务执行顺序。
- 不要依赖于特定延迟的可实现性(Nissen 和 Wallis 1984)。
- 使用延迟直到而不是延迟语句,延迟到达到特定时间为止。
- 避免使用繁忙等待循环而不是延迟。
- 仔细考虑在带标记类型继承层次结构中放置保护类型组件的位置。
- 考虑使用泛型来提供需要保护对象提供的限制的数据类型的可扩展性。
通信
[edit | edit source]- 在 rendezvous 中最小化执行的工作量。
- 最小化在任务的选择性接受循环中执行的工作。
- 考虑使用保护对象进行数据同步和通信。
- 当您无法避免使用所有备选方案都可能关闭的 selective accept 语句时,提供 Program_Error 异常的处理程序(Honeywell 1986)。
- 系统地使用针对 Tasking_Error 的处理程序。
- 做好在 rendezvous 期间处理异常的准备。
- 考虑使用 when others 异常处理程序。
- 不要依赖任务属性 'Callable 或 'Terminated 的值(Nissen 和 Wallis 1984)。
- 不要依赖属性来避免在 entry 调用上发生 Tasking_Error。
- 对于任务,不要依赖 entry 属性 'Count 的值。
- 与使用任务 entry 属性 'Count 相比,使用受保护 entry 属性 'Count 更可靠。
- 使用受保护子程序或入口的调用在任务之间传递数据,而不是使用不受保护的共享变量。
- 不要使用不受保护的共享变量作为任务同步设备。
- 不要在保护条件中引用非局部变量。
- 如果需要不受保护的共享变量,请使用 pragma Volatile 或 Atomic。
- 对任务入口使用条件入口调用时要小心。
- 谨慎使用带有 else 部分的 selective accepts。
- 不要依赖任务入口的定时入口调用中的特定延迟。
- 不要依赖带有延迟备选方案的选择性接受中的特定延迟。
- 考虑使用受保护对象来代替 rendezvous 用于面向数据的同步。
- 最大限度地减少每个任务的 accept 和 select 语句的数量。
- 最大限度地减少每个入口的 accept 语句的数量。
终止
[edit | edit source]- 考虑在每个任务内的主循环中为会合使用异常处理程序。
- 不要无意中创建非终止任务。
- 显式关闭依赖于库包的任务。
- 确认任务在使用 Ada.Unchecked_Deallocation 释放之前已终止。
- 考虑使用带终止备选方案的选择语句,而不是单独使用接受语句。
- 考虑为每个不需要 else 部分或延迟的 selective accept 提供终止备选方案。
- 在环境任务完成等待其他任务之后,不要在用户定义的 Finalize 过程中声明或创建任务。
- 避免使用 abort 语句。
- 考虑使用异步选择语句而不是 abort 语句。
- 尽量减少异步选择语句的使用。
- 避免从任务或异步选择语句的可中止部分分配非原子全局对象。
- 在任务体的末尾放置一个 others 的异常处理程序。
- 考虑让每个任务体末尾的异常处理程序报告任务的死亡。
- 不要依赖任务状态来确定是否可以与任务进行 rendezvous。
- 不要调用一个直接或间接导致调用原始调用任务的入口的任务入口。
- 在使用 Ada.Command_Line.Set_Exit_Status 过程时,避免在设置退出状态码时出现竞争条件。
- 在一个包含多个任务的程序中,封装、序列化和检查对 Ada.Command_Line.Set_Exit_Status 过程的调用。