TNxHorizon
類別是完全線程安全的NXHorizon.Instance
是執行緒安全的,可以從任何執行緒使用聲明事件類型:
事件按類型資訊 - TypeInfo
進行分類。每個單獨的事件類別都需要不同的類型。
type
TFoo = class
...
end ;
TOtherFoo = type TFoo;
TIntegerEvent = type Integer;
TStringEvent = type string;
TFooEvent = INxEvent<TFoo>;
TOtherFooEvent = INxEvent<TOtherFoo>;
訂閱/取消訂閱事件:
訂閱事件可以添加到任何現有的類別中。
type
TSubscriber = class
protected
// subscriptions
fIntegerSubscription: INxEventSubscription;
fStringSubscription: INxEventSubscription;
// event handlers
procedure OnIntegerEvent ( const aEvent: TIntegerEvent);
procedure OnStringEvent ( const aEvent: TStringEvent);
public
constructor Create;
destructor Destroy; override;
end ;
constructor TSubscriber.Create;
begin
fIntegerSubscription := NxHorizon.Instance.Subscribe<TIntegerEvent>(Async, OnIntegerEvent);
fStringSubscription := NxHorizon.Instance.Subscribe<TStringEvent>(Sync, OnStringEvent);
end ;
destructor TSubscriber.Destroy;
begin
fIntegerSubscription.WaitFor;
fStringSubscription.WaitFor;
NxHorizon.Instance.Unsubscribe(fIntegerSubscription);
NxHorizon.Instance.Unsubscribe(fStringSubscription);
inherited ;
end ;
procedure TSubscriber.OnIntegerEvent ( const aEvent: TIntegerEvent);
begin
Writeln(aEvent);
end ;
procedure TSubscriber.OnStringEvent ( const aEvent: TStringEvent);
begin
Writeln(aEvent);
end ;
發送訊息:
NxHorizon.Instance.Post<TIntegerEvent>( 5 );
NxHorizon.Instance.Send<TStringEvent>( ' abc ' , Async);
或者
var
IntEvent: TIntegerEvent;
StrEvent: TStringEvent;
IntEvent := 5 ;
StrEvent := ' abc ' ;
NxHorizon.Instance.Post(IntEvent);
NxHorizon.Instance.Send(StrEvent, Async);
事件處理程序方法必須符合以下聲明,其中T
可以是任何類型。非同步傳遞需要具有自動記憶體管理的類型或值類型。您也可以使用手動管理的長期物件實例作為事件,但在這種情況下,您必須確保它們在已分派的訊息完全處理之前不會被銷毀。
procedure( const aEvent: T) of object ;
TNxHorizonDelivery
類型聲明了四種交付選項:
Sync
- 在目前執行緒中同步Async
MainSync
- 在主執行緒上同步MainAsync
- 主執行緒異步Sync
和MainSync
是 BLOCKING 操作,事件處理程序會在目前執行緒的上下文中立即執行,或與主執行緒同步。這將阻止使用相同事件匯流排執行個體調度其他事件,直到事件處理程序完成。不要在預設事件總線實例上使用它(或僅在短期執行時謹慎使用)。
如果發送事件是從主執行緒上下文完成的, MainAsync
傳遞將使用TThread.ForceQueue
在主執行緒上下文中非同步執行事件處理程序。
訂閱事件處理程序會建構一個新的INxEventSubscription
實例。您應該儲存傳回的實例,以便稍後取消訂閱。
取消訂閱有兩種方法: Unsubscribe
和UnsubscribeAsync
。
這兩種方法都會取消訂閱並將其從事件總線中維護的訂閱集合中刪除。該集合正在Post
和Send
方法內迭代。此時不允許進行任何修改,並且可能會導致意外行為。
為了避免在迭代期間修改訂閱者集合,如果要取消訂閱在同步分派的事件處理程序中運行的程式碼,則應使用UnsubscribeAsync
,這將立即取消訂閱,但會延遲從集合中實際刪除,在外部運行它。
非同步分派的事件處理程序始終在分派迭代之外運行,並且它們允許使用Unsubscribe
方法。但是,處理程序的分派方式可以透過不相關的外部程式碼進行更改,如果您無法絕對保證非同步分派,則可以使用UnsubscribeAsync
。
Unsubscribe
和UnsubscribeAsync
也會在從訂閱集合中刪除訂閱之前取消訂閱。通常,取消訂閱之前不需要明確取消訂閱,但是如果您出於某種特殊原因想要在取消訂閱之前的某個時刻取消訂閱,則可以呼叫其Cancel
方法。可以安全地多次呼叫Cancel
。一旦取消訂閱,其狀態就無法恢復。
由於非同步事件分派,當您取消或取消訂閱特定訂閱時,可能會有一個已分派的事件處理程序。如果您取消訂閱析構函數(您的訂閱者類別析構函數),這可能會導致您在訂閱者實例銷毀過程中或銷毀後存取訂閱者實例。為了防止這種情況,您可以在訂閱上呼叫WaitFor
,這將立即取消訂閱並阻塞,直到所有分派的事件處理程序執行完畢。
如果您從主執行緒的上下文中呼叫WaitFor
,並且您的事件處理程序運行了很長時間,這將導致您的應用程式在該段時間內停止回應。
BeginWork
和EndWork
方法是訂閱等待機制的一部分。如果您需要在其他執行緒的事件處理程序內執行某些程式碼,並且需要確保也等待程式碼,則可以在啟動此類執行緒之前呼叫BeginWork
,並在其完成後呼叫EndWork
。確保所有程式碼路徑最終都會呼叫匹配的EndWork
,因為不這樣做會在呼叫WaitFor
時導致死鎖。
procedure TSubscriber.OnLongEvent ( const aEvent: TIntegerEvent);
begin
fIntegerSubscription.BeginWork;
try
TTask.Run(
procedure
begin
try
...
finally
fIntegerSubscription.EndWork;
end ;
end );
except
fIntegerSubscription.EndWork;
raise;
end ;
end ;
procedure Post <T>( const aEvent: T);
procedure Send <T>( const aEvent: T; aDelivery: TNxHorizonDelivery);
Post
方法用於發布事件,其中傳遞選項將取決於訂閱事件時設定的訂閱傳遞選項。
Send
方法重寫訂閱傳遞選項,並以由傳遞的aDelivery
參數決定的方式分派事件。如果訂閱指定在主執行緒上下文中分派,則Send
方法將滿足該要求,因此您不必擔心這些事件處理程序中的同步。
Post
或Send
是否會阻止通話取決於所使用的傳遞選項。當您使用Post
時,請注意,相同事件類型的不同訂閱可以配置不同的傳遞選項。
TNxHorizon
是一個手動管理的、完全線程安全的類別。您可以根據需要建立任意多個單獨的事件匯流排實例。實例是完全線程安全的,只要您以只讀模式使用引用,就不需要任何額外的保護 - 一旦初始化引用並開始跨線程使用該實例,就不允許修改引用變數本身。您可以從任何執行緒自由地呼叫此類引用上的任何方法。
如果您需要支援不同的通道(額外的事件分類),您可以透過為每個通道建立單獨的事件匯流排實例來實現此類功能。
TNxHorizon
類別的功能不能直接公開為接口,因為它使用介面不支援的參數化方法。
除了透過NxHorizon.Instance
提供單例實例之外,還可以將單獨的總線實例用於其他目的,但生命週期要短得多。為了簡化這些實例的生命管理並避免在多線程環境中存取懸空指針,您可以使用INxHorizon
安全地保存和共享此類事件總線實例。
這也開啟了使用事件總線實例的可能性,事件總線實例作為觀察者模式中的調度機制相當輕量級,其中可觀察的主題保存並公開其INxHorizon
引用,觀察者可以附加到該引用。訂閱時,觀察者應該儲存他們正在訂閱的INxHorizon
實例,這樣即使主題本身已同時被釋放,他們也可以安全地取消訂閱。
這允許以線程安全的方式對非自動管理實例的主題使用觀察者模式。此外,保持對事件總線實例的強(線程安全)引用而不是直接使用主題,可以避免使用託管物件實例時潛在的引用循環,而不是使用線程不安全的弱引用。
INxHorizon.Instance
傳回包裝的TNxHorizon
實例,該實例由容器手動管理。只要訂閱者對其容器持有強引用,就可以安全地使用它。
主體需要在清理過程中對其INxHorizon
引用呼叫ShutDown
方法。這會將IsActive
標誌設定為False
並將TNxHorizonShutDownEvent
發送給其訂閱者,以便他們可以執行適當的清理。 TNxHorizonShutDownEvent
包含包裝的TNxHorizon
實例,因此訂閱者可以使用單一關閉事件處理程序來管理多個主題。
呼叫ShutDown
不會對總線發送和發布訊息的能力產生任何影響。如果您需要確保在清理過程中沒有調度新事件,您可以在呼叫Post
或Send
之前檢查IsActive
標誌。
此事件匯流排利用 PPL 中的TTask
在 XE7 和更新的 Delphi 版本中非同步分派事件。這些任務在預設執行緒池上運行。這是設計使然。這是基於這樣的前提:任何使用預設執行緒池的程式碼都應該運行得非常快並且不應該引起爭用。
如果事件處理程序中的程式碼或其他程式碼使用預設池來執行可能導致問題的長時間運行的任務,那麼正確的做法是在單獨的專用執行緒池上運行該特定的長時間運行的程式碼,而不是創建多個執行緒池將服務於需要執行某些任務的框架的不同部分。
對於長時間運行的事件處理程序,問題的最快解決方案是使用同步分派並在事件處理程序程式碼內啟動一個新任務,然後可以使用其他一些非預設執行緒池。這樣,您將可以更好地控製程式碼,並可以自由地更改特定處理程序的行為,而不會影響同一事件總線實例上執行的所有其他處理程序:
procedure TSubscriber.OnLongEvent ( const aEvent: TLongEvent);
begin
TTask.Run(
procedure
begin
...
end , DedicatedThreadPool);
end ;
此事件總線實現的主要特點是線程安全、速度和簡單性。任何附加功能和擴充功能都不得損害這些最初的目標和意圖。
這個實作也是基於我自己的需求和程式碼,某些部分可能不完全滿足其他一些常見的程式碼工作流程。
由於速度是基於Post
和Send
方法的當前實現,因此我預計這些方面不會有太多變化。然而,在這兩種方法之外改進或支援不同的訂閱工作流程是可能的。
https://dalija.prasnikar.info