跳轉到內容

Ada 樣式指南/併發

來自 Wikibooks,開放世界中的開放書籍

程式設計實踐 · 可移植性

併發可以是表面的併發或真正的併發。在單處理器環境中,表面併發是併發活動交錯執行的結果。在多處理器環境中,真正的併發是併發活動重疊執行的結果。

併發程式設計比順序程式設計更難,也更容易出錯。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 過程的呼叫。

可移植性

華夏公益教科書