47、共享可恢复对象:Ada 95 中的实现与应用

共享可恢复对象:Ada 95 中的实现与应用

1. 引言

在当今的计算系统中,容错的重要性日益凸显。随着各领域对系统可靠性的需求不断增长,硬件容错技术已成为可靠计算系统的关键组成部分。与此同时,软件容错,即软件应对故障(尤其是设计故障)的能力,也变得越来越重要。

为了解决软件中的意外故障,人们提出了多种方法,如 N 版本编程和恢复块。这些机制经过改进和扩展,以处理并发问题,如事务、对话和原子操作。在许多使用向后错误恢复的方案中,通常需要能够撤销错误计算的影响,将应用程序的状态恢复到已知正确的点。在并发环境中提供这种恢复能力的对象被称为共享可恢复对象。本文将详细介绍 Ada 95 中共享可恢复对象的实现。

2. 可恢复对象
2.1 术语

可恢复对象是一种可以保存其状态,并在需要时恢复该状态的对象。保存状态有时也称为创建检查点或快照,通常在系统(包括要保存的对象)处于一致状态时执行。一旦保存,对象的状态可以在任何时候恢复,恢复的原因取决于可恢复对象的使用场景。

可以为一个对象拍摄多个快照,这意味着可以有对象状态的多个版本,并且可以回退到这些版本。但不允许分支,每个版本在给定时间最多有一个父版本和一个子版本。最新的版本称为当前版本。

共享可恢复对象可以由多个任务同时使用,这些任务称为参与任务。需要注意的是,共享可恢复对象既不提供也不强制对访问它的任务进行并发控制,应用程序程序员必须确保对对象封装的数据进行一致的访问。

2.2 使用场景

可恢复对象在多种场景中都非常有用:
- 撤销功能 :在应用程序中,在允许用户修改对象之前,可以拍摄对象状态的快照。如果用户对修改结果不满意,可以恢复对象的旧状态。多个快照允许进行多级撤销操作。
- 多任务应用 :越来越多的应用程序使用多任务来提高性能或为用户提供更好的反馈和交互。因此,可恢复对象的设计应考虑并发因素。
- 软件容错技术 :特别是使用向后错误恢复的技术,如恢复块和类事务系统,可以利用可恢复对象。
- 恢复块 :恢复块是一种基于设计多样性和向后错误恢复概念的软件结构机制。它由一个或多个实现功能的替代方案和一个验收测试组成,该测试用于确定替代方案是否正常工作。在正常情况下,只执行第一个替代方案。只有当验收测试检测到错误时,才会依次尝试其他替代方案。恢复块的典型语法如下:

ensure <acceptance test> by <primary alternate>
else by <second alternate>
else by <n'th alternate>
else error

在执行主替代方案之前,会拍摄组件当前状态的快照。当某个替代方案未通过验收测试时,会恢复该状态并尝试下一个替代方案,直到某个替代方案成功或没有更多替代方案可用。可恢复对象对于恢复块的程序员非常有用,他们可以将组件的状态封装在一个或多个可恢复对象中,并在执行主替代方案之前拍摄快照。如果主替代方案未通过验收测试,可以轻松恢复状态。
- 事务 :事务是一种用于在并发环境和存在故障的情况下确保数据一致性的机制。事务的执行具有著名的 ACID 属性:
- 原子性 :事务提供“全有或全无”的保证,即要么执行事务的所有状态更改,要么不执行任何更改。
- 一致性 :从一致状态开始执行的事务将产生另一个一致状态。
- 隔离性 :并发执行的事务(竞争并发)相互隔离,其结果必须与按某种串行顺序执行的结果相同。
- 持久性 :一旦事务成功终止,其效果将是永久的,即使发生故障也是如此。
与恢复块一样,可恢复对象可以帮助实现事务的原子性。如果事务中止,只要应用程序的状态封装在一个或多个可恢复对象中,并且在进入事务之前拍摄了快照,就可以撤销对状态的更改。嵌套事务可以使用嵌套快照。
- 对话、原子操作和协调原子操作 :这些可以看作是基本事务概念的扩展,用于处理协作并发。在所有这三种方案中,进程或任务协作并可能访问共享数据,如果发生错误并使用向后错误恢复,则必须恢复这些数据。同样,可恢复对象只有在能够在并发环境中使用时才有用。

3. Ada 95 的特性

在实现共享可恢复对象时,Ada 95 的两个特性被广泛使用:受控类型和任务标识。

3.1 受控类型

Ada 95 引入了受控类型,以在抽象数据类型中方便地进行资源管理,同时保持抽象性。语言定义的包 Ada.Finalization 声明了两个抽象标记类型: Controlled Limited Controlled 。从这两个类型之一派生的用户定义类型将继承 Initialize Finalize 和(对于 Controlled 类型) Adjust 操作。

这些操作具有特殊性质,它们会在以下情况下自动调用:
- Initialize :当创建受控类型的对象且没有显式初始化时调用。
- Finalize :在受控对象销毁之前调用,例如对象超出作用域、使用 Ada.Unchecked_Deallocation 进行释放或在赋值时被覆盖。
- Adjust :在赋值时,目标对象被覆盖后调用。

这三个原始操作的默认实现不执行任何操作。通过在派生类型中重写继承的版本,应用程序开发人员可以精确控制对象的创建、销毁和赋值。特别是能够将自动终结操作与数据类型关联起来,对于 Ada 来说非常重要,因为 Ada 注重信息隐藏,并且有多种退出作用域的方式(异常、退出、返回、中止、异步控制转移)。

3.2 任务标识和任务属性

为了标识任务类型的实例,语言定义的包 Ada.Task_Identification 定义了 Task_ID 类型。该包提供的操作允许应用程序程序员获取当前任务的 Task_ID 、中止由 Task_ID 标识的任务以及查询任务的状态信息。

还可以使用泛型包 Ada.Task_Attributes 将用户定义的信息与任务关联起来。应用程序程序员可以通过实例化该包为系统中的每个任务附加一个数据类型:

package My_Task_Info is
    new Ada.Task_Attributes
        (Attribute => My_Data_Type,
         Initial_Value => My_Initial_Value);

该包提供了设置和检索与每个任务关联的属性值的函数。当使用受控类型时,任务属性特别有用,因为通过其 Finalize 原始操作,可以在任务中止或终止时执行应用程序定义的操作。

4. 设计问题
4.1 更新策略

在实现可恢复对象时,需要在两种不同的状态更新策略之间进行选择:就地更新和延迟更新。
- 就地更新 :在就地更新中,只有可恢复对象的当前版本可见并被修改。可恢复对象可以表示为对象状态版本的列表,除了列表的头部外,其他版本对用户隐藏。对象的不同版本通常使用版本号 n 进行标识。当拍摄快照时,会复制当前版本并将其添加到隐藏状态列表中;后续的修改仍然在当前版本上进行。当需要恢复某个版本时,会将相应的状态从列表复制回当前版本。

就地更新实现通常需要提供以下操作:
- Establish Checkpoint :拍摄对象当前状态的快照(复制当前版本的状态)。
- Discard Checkpoint (n) :丢弃版本 n (从列表中移除相应的版本)。
- Restore Checkpoint (n) :通过将当前状态替换为版本 n 中存储的状态,恢复对象保存的状态。

  • 延迟更新 :在延迟更新中,拍摄快照时会创建一个新版本并返回给用户。后续的修改将应用于这个新版本。一旦用户对对象的更改满意,他可以提交修改,这将导致将新状态复制回上一个版本。

延迟更新方案通常为用户提供以下操作:
- New State (S) :复制对象的版本 S 并将新版本返回给用户。
- Discard State (S) :丢弃版本 S (从列表中移除相应的版本)。
- Commit State (S) :将对象版本 S 的内容复制回其父版本的内容,然后丢弃版本 S (从列表中移除相应的版本)。

更新策略与并发

在多个任务共享可恢复对象时,使用延迟更新方案具有很大优势。假设有多个任务在同一个对象版本上工作,突然其中一个任务决定恢复到之前保存的状态。

如果使用就地更新方案,调用 Restore Checkpoint 可能会导致不一致,因为其他任务不知道该中止操作,会继续像对象仍处于当前状态一样工作。为了避免这种问题,该任务必须首先将其决定通知所有其他任务,这给应用程序程序员带来了额外的同步负担。

而使用延迟更新方案,想要恢复到上一个版本 S 的任务可以安全地对当前版本调用 Discard State ,然后开始使用版本 S ,因为对象的所有版本都是可见的。如果需要,它甚至可以创建版本 S 的新子版本。与此同时,其他任务可以继续工作,并在尝试提交其更改时得知中止操作。

为了在并发情况下正确处理恢复,可恢复对象必须知道所有操作过它的任务的数量和身份。这是确定何时可以丢弃对象的某个版本并释放与之关联的内存的唯一方法。因此,任务必须以某种方式注册为可恢复对象某个版本的用户。

可恢复对象的规则

基于以上考虑,共享可恢复对象遵循以下规则:
- 多个任务可以同时处理可恢复对象的一个版本。除了创建该版本的任务外,所有其他任务必须明确注册为该版本的参与者。
- 一旦参与任务对可恢复对象的某个版本调用了 Discard State ,无论其他参与者的投票如何,该版本都将被丢弃。
- 只有当所有参与任务都调用 Commit State 时,一个版本才被视为已提交,并且其状态将被复制回其父版本。
- 一个版本在给定时间只能有一个未被丢弃的子版本。这意味着只有在第一个版本的至少一个参与者调用 Discard State 之后,才能创建该版本的第二个子版本。
- 未调用 Commit State Discard State 就消失的参与者(遗弃者)将被忽略。如果一个版本的唯一参与者消失,该版本将被丢弃。

4.2 接口

基于上述讨论,下面描述共享可恢复对象实现的接口。下面描述的可恢复对象类允许应用程序将任意对象转换为可恢复对象。包 Recoverable 声明了一个抽象标记类型 Recoverable_Object ,并提供了所有必要的恢复函数:

package Recoverable is
    type Recoverable_Object is abstract tagged private;
    -- Although this type is not limited, assignment of versions
    -- is not allowed (Program_Error will be raised at run time)
    type Recoverable_Object_Ref is access all Recoverable_Object'Class;

    procedure New_State (Object : in Recoverable_Object_Ref;
                         New_Object : out Recoverable_Object_Ref);
    procedure Join_State (Object : in Recoverable_Object_Ref);
    procedure Commit_State (Object : in out Recoverable_Object_Ref;
                            Sync : in Boolean := False);
    procedure Discard_State (Object : in out Recoverable_Object_Ref);

    type Object_Status_Type is (Undecided, Discarded, Committing);
    function Current_Status (Object : in Recoverable_Object_Ref)
                             return Object_Status_Type;
    -- 异常声明部分,原文未完整给出,这里保持原文格式
    Object_Discarded,
    Already_Created_Subversion,
    Task_Already_Joined,
    Task_Never_Joined,

通过这个接口,应用程序可以方便地使用可恢复对象,并进行状态的创建、加入、提交和丢弃等操作。同时,还可以查询对象的当前状态。

下面通过一个流程图来总结可恢复对象的操作流程:

graph TD;
    A[开始] --> B[New_State创建新版本];
    B --> C{是否参与任务};
    C -- 是 --> D[Join_State加入状态];
    C -- 否 --> E[直接操作];
    D --> F{是否满意更改};
    E --> F;
    F -- 是 --> G[Commit_State提交状态];
    F -- 否 --> H[Discard_State丢弃状态];
    G --> I[结束];
    H --> J[恢复到之前状态];
    J --> B;

这个流程图展示了可恢复对象的基本操作流程,从创建新版本开始,根据任务是否参与进行不同的处理,然后根据用户对更改的满意度决定是提交还是丢弃状态。如果丢弃状态,则可以恢复到之前的状态并重新开始操作。

通过以上对共享可恢复对象的介绍,我们了解了其基本概念、使用场景、Ada 95 的相关特性、更新策略以及接口设计。这些知识将有助于我们在 Ada 95 中更好地实现和应用共享可恢复对象,提高软件的容错能力和并发处理能力。

在后续的内容中,我们将进一步探讨共享可恢复对象在多任务应用中的具体示例,以及实现的内部细节。同时,我们还将总结本文的主要内容,并展望未来的工作方向。

共享可恢复对象:Ada 95 中的实现与应用

5. 多任务应用示例

为了更好地理解共享可恢复对象在实际中的应用,下面给出一个使用多任务的示例。假设我们有一个简单的银行账户系统,多个任务可以同时对账户进行操作,如存款、取款等。我们将使用可恢复对象来确保在出现错误时可以恢复账户的状态。

with Ada.Text_IO; use Ada.Text_IO;
with Recoverable;

procedure Bank_Account_Example is
    -- 定义账户类型
    type Account_Type is tagged record
        Balance : Integer := 0;
    end record;

    -- 将账户类型转换为可恢复对象
    package Account_Recoverable is new Recoverable.Generic_Recoverable (Account_Type);
    use Account_Recoverable;

    -- 账户引用
    Account : Recoverable_Object_Ref;

    -- 存款任务
    task type Deposit_Task is
        entry Start (Acc : Recoverable_Object_Ref; Amount : Integer);
    end Deposit_Task;

    task body Deposit_Task is
        Acc : Recoverable_Object_Ref;
        Amount : Integer;
    begin
        accept Start (Acc : Recoverable_Object_Ref; Amount : Integer) do
            Deposit_Task.Acc := Acc;
            Deposit_Task.Amount := Amount;
        end Start;

        -- 创建新版本
        declare
            New_Acc : Recoverable_Object_Ref;
        begin
            New_State (Acc, New_Acc);
            Join_State (New_Acc);

            -- 进行存款操作
            Account_Type (New_Acc.all).Balance := Account_Type (New_Acc.all).Balance + Amount;

            -- 提交更改
            Commit_State (New_Acc);
        end;
    end Deposit_Task;

    -- 取款任务
    task type Withdraw_Task is
        entry Start (Acc : Recoverable_Object_Ref; Amount : Integer);
    end Withdraw_Task;

    task body Withdraw_Task is
        Acc : Recoverable_Object_Ref;
        Amount : Integer;
    begin
        accept Start (Acc : Recoverable_Object_Ref; Amount : Integer) do
            Withdraw_Task.Acc := Acc;
            Withdraw_Task.Amount := Amount;
        end Start;

        -- 创建新版本
        declare
            New_Acc : Recoverable_Object_Ref;
        begin
            New_State (Acc, New_Acc);
            Join_State (New_Acc);

            -- 进行取款操作
            if Account_Type (New_Acc.all).Balance >= Amount then
                Account_Type (New_Acc.all).Balance := Account_Type (New_Acc.all).Balance - Amount;
                -- 提交更改
                Commit_State (New_Acc);
            else
                -- 余额不足,丢弃状态
                Discard_State (New_Acc);
            end if;
        end;
    end Withdraw_Task;

begin
    -- 创建账户
    Account := new Account_Type'(Balance => 1000);

    -- 创建可恢复对象
    declare
        Rec_Account : Recoverable_Object_Ref;
    begin
        New_State (Account, Rec_Account);
        Account := Rec_Account;
    end;

    -- 创建存款和取款任务
    declare
        Deposit : Deposit_Task;
        Withdraw : Withdraw_Task;
    begin
        Deposit.Start (Account, 500);
        Withdraw.Start (Account, 200);
    end;

    -- 等待任务完成
    delay 1.0;

    -- 输出最终账户余额
    Put_Line ("Final Balance: " & Integer'Image (Account_Type (Account.all).Balance));
end Bank_Account_Example;

在这个示例中,我们首先定义了一个 Account_Type 类型来表示银行账户,然后使用 Recoverable.Generic_Recoverable 包将其转换为可恢复对象。接着,我们创建了两个任务: Deposit_Task Withdraw_Task ,分别用于存款和取款操作。每个任务在操作前都会创建一个新版本的账户对象,操作完成后根据情况提交或丢弃状态。

下面是这个示例的操作步骤总结:
1. 定义账户类型 Account_Type
2. 使用 Recoverable.Generic_Recoverable 包将账户类型转换为可恢复对象。
3. 创建账户并将其转换为可恢复对象。
4. 创建存款和取款任务,并传入账户引用和操作金额。
5. 每个任务在操作前创建新版本,操作完成后根据情况提交或丢弃状态。
6. 等待任务完成,输出最终账户余额。

通过这个示例,我们可以看到可恢复对象如何在多任务环境中确保数据的一致性和容错性。

6. 实现的内部细节

在了解了共享可恢复对象的基本概念、使用场景和应用示例后,我们来深入探讨其实现的内部细节。

6.1 受控类型的实现

受控类型在可恢复对象的实现中起着关键作用。在 Ada.Finalization 包中, Controlled Limited Controlled 类型提供了对象创建、销毁和赋值的基本操作。在可恢复对象的实现中,我们需要重写这些操作以实现状态的保存和恢复。

package body Recoverable is
    type Recoverable_Object is abstract tagged record
        -- 内部状态信息
        State_List : State_List_Type;
        Current_Version : Version_Number;
    end record;

    -- 重写 Initialize 操作
    overriding procedure Initialize (Object : in out Recoverable_Object) is
    begin
        -- 初始化状态列表和当前版本
        Object.State_List := Empty_State_List;
        Object.Current_Version := Initial_Version;
    end Initialize;

    -- 重写 Finalize 操作
    overriding procedure Finalize (Object : in out Recoverable_Object) is
    begin
        -- 释放状态列表占用的资源
        Free_State_List (Object.State_List);
    end Finalize;

    -- 重写 Adjust 操作
    overriding procedure Adjust (Object : in out Recoverable_Object) is
    begin
        -- 复制状态列表和当前版本
        Object.State_List := Copy_State_List (Object.State_List);
        Object.Current_Version := Object.Current_Version;
    end Adjust;
end Recoverable;

在这个实现中,我们重写了 Initialize Finalize Adjust 操作。 Initialize 操作用于初始化可恢复对象的状态列表和当前版本; Finalize 操作在对象销毁时释放状态列表占用的资源; Adjust 操作在对象赋值时复制状态列表和当前版本。

6.2 任务标识和属性的使用

任务标识和属性在可恢复对象的并发处理中非常重要。通过 Ada.Task_Identification Ada.Task_Attributes 包,我们可以标识任务并关联用户定义的信息。

-- 任务标识和属性的使用示例
with Ada.Task_Identification; use Ada.Task_Identification;
with Ada.Task_Attributes;

package Task_Info is
    type Task_Info_Type is record
        Version : Version_Number;
        Is_Participant : Boolean;
    end record;

    package My_Task_Attributes is new Ada.Task_Attributes (Task_Info_Type);
    use My_Task_Attributes;

    procedure Register_Task (Acc : Recoverable_Object_Ref; Version : Version_Number) is
        Task_Info : Task_Info_Type;
    begin
        Task_Info.Version := Version;
        Task_Info.Is_Participant := True;
        Set_Attribute (Current_Task, Task_Info);
        -- 注册任务为版本的参与者
        Register_Participant (Acc, Version, Current_Task);
    end Register_Task;

    function Get_Task_Version return Version_Number is
        Task_Info : Task_Info_Type;
    begin
        Task_Info := Get_Attribute (Current_Task);
        return Task_Info.Version;
    end Get_Task_Version;
end Task_Info;

在这个示例中,我们定义了一个 Task_Info_Type 类型来存储任务的信息,包括关联的版本号和是否为参与者。然后使用 Ada.Task_Attributes 包将这个信息与任务关联起来。 Register_Task 过程用于注册任务为某个版本的参与者,并将任务信息存储在任务属性中; Get_Task_Version 函数用于获取任务关联的版本号。

7. 总结与展望

通过本文的介绍,我们全面了解了共享可恢复对象在 Ada 95 中的实现与应用。从基本概念、使用场景、Ada 95 的相关特性、更新策略、接口设计,到多任务应用示例和实现的内部细节,我们逐步深入探讨了这一主题。

共享可恢复对象为软件的容错和并发处理提供了强大的支持。在多任务环境中,它可以确保数据的一致性和在出现错误时恢复状态的能力。通过使用 Ada 95 的受控类型和任务标识特性,我们可以方便地实现可恢复对象的功能。

未来,我们可以进一步研究和改进共享可恢复对象的实现。例如,可以优化更新策略,提高并发处理的效率;可以扩展接口,提供更多的功能,如支持更复杂的事务处理;还可以将可恢复对象的概念应用到更多的领域,如分布式系统、实时系统等。

总之,共享可恢复对象是一个有很大潜力的技术,它将有助于提高软件的可靠性和可维护性,为软件开发带来更多的可能性。

下面通过一个表格总结本文的主要内容:
| 主题 | 内容 |
| ---- | ---- |
| 可恢复对象概念 | 可保存和恢复状态的对象,支持多任务并发访问 |
| 使用场景 | 撤销功能、多任务应用、软件容错技术(恢复块、事务等) |
| Ada 95 特性 | 受控类型(Initialize、Finalize、Adjust 操作)、任务标识和属性 |
| 更新策略 | 就地更新和延迟更新,延迟更新在并发环境中更具优势 |
| 接口设计 | Recoverable 包提供创建、加入、提交、丢弃状态等操作和状态查询功能 |
| 多任务应用示例 | 银行账户系统,展示可恢复对象在多任务环境中的应用 |
| 实现内部细节 | 受控类型的重写操作、任务标识和属性的使用 |

通过这个表格,我们可以更清晰地看到本文的主要内容结构和重点。希望本文的内容能够对读者在 Ada 95 中实现和应用共享可恢复对象提供有价值的参考。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值