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:編譯指示限制和編譯指示配置檔案 [帶註釋的]
- Ada 程式設計
- Ada 程式設計/庫/Ada.Storage IO
- Ada 程式設計/庫/Ada.Task_Identification
- Ada 程式設計/庫/Ada.Task_Attributes
- 第 4 章:程式結構
- 第 6 章:併發
