Orleans入门
一、Grains
Grains是Orleans編程模型的關鍵原語。 Grains是Orleans應用程序的構建塊,它們是隔離,分配和持久性的原子單元。 Grains是表示應用程序實體的對象。 就像在經典的面向對象編程(Object Oriented Programming)中一樣,grain封裝實體的狀態并在代碼邏輯中對其行為進行編碼。 Grains可以持有對方的引用,并通過調用通過接口公開的對方的方法進行交互。
注意:在操作系統中叫原語,是執行過程中不可被打斷的基本操作,你可以理解為一段代碼,這段代碼在執行過程中不能被打斷(在多道程序設計里進程間相互切換,還可能有中斷發生,經常會被打斷)。像原子一樣具有不可分割的特性, 所以叫原語, 像原子一樣的語句
Orleans的目標是大大簡化構建可擴展的應用程序,并消除大部分的并發挑戰
除了通過消息傳遞之外,不在grains實例之間共享數據。
通過提供單線程執行保證每個單獨的grain。
典型的grain封裝單個實體(例如特定用戶或設備或會話)的狀態和行為。
1,Grain身份
個體grain是grain類型(類)的一個唯一尋址實例。 每個grain在其類型中都有一個唯一的標識,也被稱為grain鍵。 其類型中的Grain標識可以是一個長整型,一個GUID,一個字符串,或者一個長+字符串或GUID +字符串的組合。??
2,訪問Grain
grain類實現一個或多個grain接口,與這種類型的grain進行交互的形式代碼契約。 為了調用grain,調用者需要知道grain類實現的grain接口,包括調用者想要調用的方法和目標grain的唯一標識(關鍵字)。 例如,以下是如何使用用戶配置文件grain來更新用戶的地址,如果電子郵件用作用戶身份。
var user = grainFactory.GetGrain<IUserProfile>(userEmail);await user.UpdateAddress(newAddress);調用GetGrain是一個廉價的本地操作,構建一個具有嵌入標識和目標grain類型的grain參考。
注意,不需要創建或實例化目標grain。我們調用它來更新用戶的地址,就好像用戶的grain已經為我們實例化了一樣。這是奧爾良編程模型最大的優點之一——我們從不需要創建、實例化或刪除grains。我們可以編寫代碼,就像所有可能的grains一樣,例如數百萬的用戶配置文件,總是在內存中等待我們調用它們。在幕后,Orleans運行時執行所有繁重的管理資源,透明地將grains帶到內存中。
3,幕后 - Grain生命周期
Grains居住在稱為倉儲的執行容器中。 倉儲形成了一個集多個物理或虛擬機資源的集群。 當工作(請求)grain時,Orleans確保在群集中的一個倉儲中有一個grain實例。 如果任何倉儲上沒有grain實例,則Orleans運行時創建一個。 這個過程被稱為激活。 在grain使用Grain持久性的情況下,運行時會在激活時自動從后備存儲中讀取狀態。
在倉儲上激活后,grain會處理來自其他grain或群集外(通常來自前端Web服務器)的傳入請求(方法調用)。 在處理請求的過程中,grain可能會調用其他grain或一些外部服務。 如果grain停止接收請求并保持空閑狀態,在可配置的不活動時間段之后,Orleans從內存中刪除grain(取消激活)以釋放其他grains的資源。 如果當有新的請求時,奧爾良會再次激活它,可能在不同的倉儲,所以調用者得到的印象是,gran一直留在內存中。 grain從存在的整個生命周期開始,只有存儲器中的持久化狀態(如果有的話)在內存中被實例化,才能從內存中移除。
Orleans控制透明地激活和停用Grains的過程。 編碼谷物時,開發者認為所有Grains總是被激活。
Grain生命周期中關鍵事件的順序如下所示。
另一個Grain或客戶端調用Grain的方法(通過Grain參考)
grain被激活(如果它還沒有在集群中的某個地方被激活),并創建一個grain類的實例,稱為grain激活
如果適用的話,grain的構造者是利用依賴注入來執行的
如果使用聲明性持久性,則從存儲中讀取grain狀態
如果重寫,則調用OnActivateAsync
grain處理傳入的請求
grain保持閑置一段時間
倉儲運行時間決定停用grain
倉儲運行時調用OnDeactivateAsync,如果重寫
倉儲運行時間從內存中刪除grain
一個倉儲的正常關閉后,所有的grain激活它停止。 任何等待在grain隊列中處理的請求都會被轉發到集群中的其他倉儲,在那里根據需要創建停用的grain的新激活。 如果一個倉儲關閉或死亡失敗,集群中的其他倉儲檢測到失敗,并開始創建失敗的倉儲上丟失的新的激活,因為這些grain的新的請求到達。 請注意,檢測倉儲故障需要一些時間(可配置),因此重新激活丟失谷物的過程不是即時的。
?
4,Grain執行
grain激活在塊中執行工作,并在每個塊執行到下一段之前完成。大量的工作內容包括了對其他grains或外部客戶的請求的方法調用,以及在完成前一段時間的關閉計劃。與一大塊工作相對應的基本執行單位稱為turn。
雖然Orleans可能會執行很多次并行的不同激活,但每次激活都將一次執行一次。這意味著不需要使用鎖或其他同步方法來防止數據競爭和其他多線程危害。?
?
二、開發一個Grain
1,設置
在編寫代碼來實現Grain類之前,在Visual Studio中創建一個新的以.NET 4.6.1或更高版本為目標的類庫項目,并向其中添加?Microsoft.Orleans.OrleansCodeGenerator.Build? NuGet包。
PM> Install-Package Microsoft.Orleans.OrleansCodeGenerator.Build2,Grain接口和類
Grain彼此交互,并通過調用聲明為各自的Grain接口的一部分方法從外部被調用。 Grain類實現一個或多個先前聲明的Grain接口。 Grains接口的所有方法必須返回一個Task(對于void方法)或Task <T>(對于返回類型T的值的方法)。
以下是Presence Service示例的摘錄:
//一個Grain界面的例子
public interface IPlayerGrain : IGrainWithGuidKey
{
? Task<IGameGrain> GetCurrentGame();
? Task JoinGame(IGameGrain game);
? Task LeaveGame(IGameGrain game);
}
//一個Grain類實現Grain接口的例子
public class PlayerGrain : Grain, IPlayerGrain
{
? ? private IGameGrain currentGame;
? ? // 玩家目前所在的游戲??赡転榭铡?/p>
? ? public Task<IGameGrain> GetCurrentGame()
? ? {
? ? ? ?return Task.FromResult(currentGame);
? ? }
? ? //游戲谷歌調用此方法通知玩家已加入游戲。
? ? public Task JoinGame(IGameGrain game)
? ? {
? ? ? ?currentGame = game;
? ? ? ?Console.WriteLine(
? ? ? ? ? ?"Player {0} joined game {1}",?
? ? ? ? ? ?this.GetPrimaryKey(),
? ? ? ? ? ?game.GetPrimaryKey());
? ? ? ?return Task.CompletedTask;
? ? }
? ?//游戲谷歌稱這種方法通知玩家已經離開游戲。
? ?public Task LeaveGame(IGameGrain game)
? ?{
? ? ? ?currentGame = null;
? ? ? ?Console.WriteLine(
? ? ? ? ? ?"Player {0} left game {1}",
? ? ? ? ? ?this.GetPrimaryKey(),
? ? ? ? ? ?game.GetPrimaryKey());
? ? ? ?return Task.CompletedTask;
? ?}
}
3,從Grains方法返回值
返回類型T值的grain方法在grain接口中定義為返回Task <T>。 對于未使用async關鍵字標記的grain方法,當返回值可用時,通常通過以下語句返回:
public Task<SomeType> GrainMethod1() {... return Task.FromResult(<variable or constant with result>); }一個沒有返回值的grain方法,實際上是一個void方法,在grain接口中被定義為返回Task。 返回的Task表示異步執行和方法的完成。 對于沒有用async關鍵字標記的grain方法,當“void”方法完成它的執行時,它需要返回Task.CompletedTask的特殊值:
public?Task?GrainMethod2()?{... return Task.CompletedTask; }標記為async的grain方法直接返回值:
public async Task<SomeType> GrainMethod3() {... return <variable or constant with result>; }標記為async的“void”grain方法不返回任何值,只是在執行結束時返回:
public?async?Task?GrainMethod4()?{... return; }如果一個grain方法從另一個異步方法調用接收到返回值,并且不需要執行該調用的錯誤處理,那么它可以簡單地將它從該異步調用接收的Task作為返回值返回:
public Task<SomeType> GrainMethod5()
{
? ? ...
? ? Task<SomeType> task = CallToAnotherGrain();
? ? return task;
}
類似地,“void”grain方法可以返回一個Task,而不是等待它,而是通過另一個調用返回給它。
public Task GrainMethod6()
{
? ? ...
? ? Task task = CallToAsyncAPI();
? ? return task;
}
4,Grain引用
Grain引用是一個代理對象,它實現與相應的grain類相同的grain接口。 它封裝了目標grain的邏輯標識(類型和唯一鍵)。 Grain引用是用來調用目標Grain的。 每個Grain引用都是針對一個Grain(Grain類的單個實例),但是可以為同一個Grain創建多個獨立的引用。
由于Grain引用代表目標Grain的邏輯身份,它獨立于Grain的物理位置,并且即使在系統完全重新啟動之后也保持有效。 開發人員可以像使用其他.NET對象一樣使用Grain引用。 它可以傳遞給一個方法,用作方法的返回值等等,甚至保存到持久存儲中。
通過將Grain身份傳遞給GrainFactory.GetGrain <T>(key)方法,可以獲得Grain引用,其中T是Grain接口,而鍵是類型內Grain的唯一鍵。
以下是如何獲取上面定義的IPlayerGrain接口的Grain引用的示例。
①從Grain里面使用的方式:
//構建特定玩家的Grain引用IPlayerGrain player = GrainFactory.GetGrain<IPlayerGrain>(playerId);②從Orleans客戶端代理里面使用的方式
在1.5.0版本之前:?IPlayerGrain player = GrainClient.GrainFactory.GetGrain<IPlayerGrain>(playerId);?
自1.5.0版本之后:?IPlayerGrain player = client.GetGrain<IPlayerGrain>(playerId);?
5,Grain方法調用
Orleans編程模型基于Async和Await異步編程。
使用前面例子中的Grain引用,下面是一個如何執行Grain方法調用:
//異步調用grain方法
Task joinGameTask = player.JoinGame(this);
//await關鍵字有效地使方法的其余部分在稍后的時間點(在等待完成的任務完成時)異步執行,而不會阻塞線程。
await joinGameTask;
//joinGameTask完成后,下一行將執行。
players.Add(playerId);
可以加入兩個或多個任務; 連接操作將創建一個新的“任務”,該任務在其所有組成任務完成時解決。 當Grain需要啟動多個計算并等待所有的計算完成之前,這是一個有用的模式。 例如,生成由多個部分組成的網頁的前端頁面可能會進行多個后端調用,每個部分一個,并為每個結果接收一個任務。 Grain將等待所有這些任務的加入; 當加入任務被解決時,單獨的任務已經完成,并且已經接收到格式化網頁所需的所有數據。
例子:
List<Task> tasks = new List<Task>();
Message notification = CreateNewMessage(text);
foreach (ISubscriber subscriber in subscribers)
{
? ?tasks.Add(subscriber.Notify(notification));
}
// WhenAll加入一個任務集合,并返回一個聯合任務,將解決所有單個通知任務時將解決。
Task joinedTask = Task.WhenAll(tasks);
await joinedTask;
// 方法的其余部分的執行將在joinedTask解析后繼續異步執行。
6,虛方法
一個Grain類可以選擇重寫OnActivateAsync和OnDeactivateAsync虛擬方法,這些方法被Orleans的運行時激活和取消激活時調用。這讓Grain代碼有機會進行額外的初始化和清理工作。OnActivateAsync引發的異常會使激活過程失敗。雖然OnActivateAsync(如果被重寫)總是被稱為Grain激活過程的一部分,OnDeactivateAsync不能保證在所有情況下都被調用,例如,在服務器故障或其他異常事件時。因此,應用程序不應該依賴OnDeactivateAsync來執行關鍵操作,例如狀態更改的持久性,只能用于最佳操作。
?
三、開發一個客戶端
1,什么是Grain客戶端?
術語“客戶端”或有時“Grain客戶端”用于與Grains相互作用的應用代碼,但其本身不是Grain邏輯的一部分。 客戶端代碼運行在托管Grains的稱為倉儲的Orleans服務器群集之外。 因此,客戶作為連接器或通往集群和應用程序的所有Grains的通道。
通常,前端Web服務器上使用客戶端連接到作為中間層的Orleans群集,并執行業務邏輯。 在典型的設置中,前端Web服務器:
接收Web請求
執行必要的身份驗證和授權驗證
決定哪些Grain(s)應該處理請求
使用Grain Client對Grain(s)進行一個或多個方法調用
處理Grain調用的成功完成或失敗以及任何返回的值
發送Web請求的響應
?2,Grain Client的初始化
在Grain客戶端可以用于調用Orleans集群托管的Grain之前,需要對Grain客戶端進行配置,初始化和連接到集群。
通過ClientConfiguration對象提供配置,該對象包含用于以編程方式配置客戶端的配置屬性層次結構。 還有一種方法可以通過XML文件來配置客戶端,但該選項將來會被棄用。 更多信息在“客戶端配置”指南中。 在這里,我們將簡單地使用一個幫助器方法來創建一個硬編碼的配置對象,用于連接到作為本地主機運行的本地倉儲
ClientConfiguration clientConfig = ClientConfiguration.LocalhostSilo();一旦我們有了一個配置對象,我們可以通過ClientBuilder類建立一個客戶端。
IClusterClient client = new ClientBuilder().UseConfiguration(clientConfig).Build();最后,我們需要在構建的客戶端對象上調用Connect()方法,使其連接到Orleans集群。 這是一個返回任務的異步方法。 所以我們需要等待完成,等待或者.Wait()
await client.Connect();3,調用Grains
從客戶端調用Grain與從Grain代碼中進行這種調用確實沒有區別。 在這兩種情況下,使用相同的GetGrain <T>(key)方法(其中T是目標Grain接口)來獲取Grain引用。 微小的差異在于通過我們調用GetGrain的工廠對象。 在客戶端代碼中,我們通過連接的客戶端對象來實現。
IPlayerGrain player = client.GetGrain<IPlayerGrain>(playerId);Task t = player.JoinGame(game)await t;對grain方法的調用返回一個Task或Task <T>,按照grain接口規則的要求。 客戶端可以使用await關鍵字異步等待返回的Task而不阻塞線程,或者在某些情況下用Wait()方法阻塞當前的執行線程。
從客戶端代碼和其他Grain中調用Grain的主要區別在于Grain的單線程執行模式。 Grain被Orleans運行時限制為單線程,而客戶端可能是多線程的。 Orleans并沒有在客戶端提供任何這樣的保證,所以客戶可以使用任何適合其環境的同步結構(鎖,事件,任務等)來管理自己的并發。
4,接收通知
有些情況下,一個簡單的請求 - 響應模式是不夠的,客戶端需要接收異步通知。 例如,用戶可能希望在她正在關注的人發布新消息時收到通知。
觀察者就是這樣一種機制,可以將客戶端對象暴露為Gtains目標以被Grain調用。對觀察者的調用不提供任何成功或失敗的指示,因為它們被發送為單向的最佳工作消息。因此,在必要的地方,應用程序代碼負責在觀察者之上構建更高級別的可靠性機制。
另一種可用于向客戶端傳遞異步消息的機制是Streams。 數據流暴露了單個消息傳遞成功或失敗的跡象,從而使可靠的通信回到客戶端。
5,例子:
這是上面給出的客戶端應用程序的一個擴展版本,連接到Orleans,查找玩家帳戶,訂閱游戲會話的更新(玩家是觀察者的一部分),并打印出通知,直到手動終止程序。
namespace PlayerWatcher
{
? ? class Program
? ? {
? ? ? ? /// <summary>
? ? ? ? /// 模擬連接到特定玩家當前所參與的游戲的同伴應用程序,并訂閱接收關于其進度的實時通知。.
? ? ? ? /// </summary>
? ? ? ? static void Main(string[] args)
? ? ? ? {
? ? ? ? ? ? RunWatcher().Wait();
? ? ? ? ? ? //阻塞主線程,以便進程不退出。更新到達線程池線程。
? ? ? ? ? ? Console.ReadLine();
? ? ? ? }
? ? ? ? static async Task RunWatcher()
? ? ? ? {
? ? ? ? ? ? try
? ? ? ? ? ? {
? ? ? ? ? ? ? ? // 連接到本地倉儲
? ? ? ? ? ? ? ? var config = ClientConfiguration.LocalhostSilo();
? ? ? ? ? ? ? ? var client = new ClientBuilder().UseConfiguration(config).Build();
? ? ? ? ? ? ? ? await client.Connect();
? ? ? ? ? ? ? ? // 硬編碼的玩家ID
? ? ? ? ? ? ? ? Guid playerId = new Guid("{2349992C-860A-4EDA-9590-000000000006}");
? ? ? ? ? ? ? ? IPlayerGrain player = client.GetGrain<IPlayerGrain>(playerId);
? ? ? ? ? ? ? ? IGameGrain game = null;
? ? ? ? ? ? ? ? while (game == null)
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? Console.WriteLine("為玩家準備當前的游戲 {0}...", playerId);
? ? ? ? ? ? ? ? ? ? try
? ? ? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? ? ? game = await player.GetCurrentGame();
? ? ? ? ? ? ? ? ? ? ? ? if (game == null) // 等待玩家加入游戲
? ? ? ? ? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? ? ? ? ? await Task.Delay(5000);
? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? catch (Exception exc)
? ? ? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? ? ? Console.WriteLine("Exception: ", exc.GetBaseException());
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? Console.WriteLine("訂閱更新游戲 {0}...", game.GetPrimaryKey());
? ? ? ? ? ? ? ? // 訂閱的更新
? ? ? ? ? ? ? ? var watcher = new GameObserver();
? ? ? ? ? ? ? ? await game.SubscribeForGameUpdates(
? ? ? ? ? ? ? ? ? ? await client.CreateObjectReference<IGameObserver>(watcher));
? ? ? ? ? ? ? ? Console.WriteLine("成功訂閱。按<輸入>停止。");
? ? ? ? ? ? }
? ? ? ? ? ? catch (Exception exc)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? Console.WriteLine("意想不到的錯誤: {0}", exc.GetBaseException());
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? /// <summary>
? ? ///實現Observer接口的Observer類。 需要將Grain引用傳遞給此類的實例以訂閱更新。
? ? /// </summary>
? ? class GameObserver : IGameObserver
? ? {
? ? ? ? // 接收更新
? ? ? ? public void UpdateGameScore(string score)
? ? ? ? {
? ? ? ? ? ? Console.WriteLine("New game score: {0}", score);
? ? ? ? }
? ? }
? ? }
}
四、運行應用程序
1,Orleans應用
如上一個主題所述,一個典型的Orleans應用程序由Grains所在的一組服務器進程(倉儲)和一組客戶進程(通常是Web服務器)組成,這些客戶進程接收外部請求,將其轉換為Grain方法調用,以及 返回結果。 因此,運行Orleans應用程序需要做的第一件事就是啟動一個倉儲群。 出于測試目的,群集可以由單個倉儲組成。 為了實現可靠的生產部署,我們顯然希望集群中有多個倉儲用于容錯和擴展。
集群運行后,我們可以啟動一個或多個連接到集群的客戶端進程,并可以向Grains發送請求。 客戶端連接到倉儲網關上的特殊TCP端點。 默認情況下,群集中的每個倉儲都啟用了客戶端網關。 因此,客戶可以并行連接到所有倉儲,以獲得更好的性能和彈性。
2,配置和啟動倉儲
一個倉儲通過一個ClusterConfiguration對象以編程方式進行配置。 它可以被實例化和直接填充,從一個文件加載設置,或者為不同的部署環境使用幾個可用的幫助器方法創建。 對于本地測試,最簡單的方法是使用ClusterConfiguration.LocalhostPrimarySilo()輔助方法。 然后將配置對象傳遞給SiloHost類的新實例,可以在此之后進行初始化和啟動。
您可以創建一個空的控制臺應用程序項目,以.NET Framework 4.6.1或更高版本為主機。 將Microsoft.Orleans.Server NuGet元數據包添加到項目。
PM> Install-Package Microsoft.Orleans.Server下面是一個如何開始本地倉儲的例子:
var siloConfig = ClusterConfiguration.LocalhostPrimarySilo();?
var silo = new SiloHost("Test Silo", siloConfig);?
silo.InitializeOrleansSilo();?
silo.StartOrleansSilo();
Console.WriteLine("按Enter鍵關閉。");?
// 在這里阻止
Console.ReadLine();?
// 我們完成后關閉筒倉。
silo.ShutdownOrleansSilo();
3,配置和連接客戶端
用于連接到倉儲集群并向Grains發送請求的客戶端通過ClientConfiguration對象和ClientBuilder以編程方式進行配置。 ClientConfiguration對象可以實例化并直接填充,從一個文件加載設置,或者為不同的部署環境使用多個可用的幫助器方法創建。 對于本地測試,最簡單的方法是使用ClientConfiguration.LocalhostSilo()輔助方法。 然后將配置對象傳遞給ClientBuilder類的新實例。
ClientBuilder公開了更多配置其他客戶端功能的方法。 之后調用ClientBuilder對象的Build方法來獲得IClusterClient接口的實現。 最后,我們調用返回對象上的Connect()方法來連接到集群。
您可以創建一個空的控制臺應用程序項目,以.NET Framework 4.6.1或更高版本為目標來運行客戶端,或者重新使用您創建的控制臺應用程序項目托管一個倉儲。 將Microsoft.Orleans.Client NuGet元程序包添加到項目。
PM> Install-Package Microsoft.Orleans.Client以下是一個客戶端如何連接到本地倉儲的例子:
var config = ClientConfiguration.LocalhostSilo();var builder = new ClientBuilder().UseConfiguration(config).var client = builder.Build();await client.Connect();4,生產配置
我們在這里使用的配置示例是用于測試與本地主機在同一臺機器上運行的倉儲和客戶機。 在生產中,倉儲和客戶機通常運行在不同的服務器上,并配置有一個可靠的群集配置選項。 有關詳細信息,請參閱Configuration Guide和Cluster Management的說明。?
五、調試
1,調試
在開發過程中,基于Orleans的應用程序可以在開發過程中使用調試器來調試程序或客戶進程。為了快速開發迭代,使用單獨的進程來結合倉儲和客戶端是很方便的,例如,由Orleans Dev/Test Host項目模板創建的控制臺應用程序項目,它是Microsoft Orleans工具擴展Visual Studio的一部分。當在Azure計算模擬器中運行時,可以將調試器附加到Worker / Web Role實例進程。
在生產環境中,在一個斷點處停止倉儲幾乎不是一個好主意,因為凍結的倉儲很快就會被集群成員協議投票死掉,并且將無法與集群中的其他倉儲進行通信。 因此,在生產追蹤是主要的“調試”機制。
2,來源鏈接
從2.0 - beta1版本開始,我們增加了對符號的源鏈接支持。這意味著,如果一個項目使用了新Orleans的NuGet軟件包,在調試應用程序代碼時,它們就可以進入Orleans源代碼。在Steve Gordon的博客文章中,您可以看到配置它需要哪些步驟。
相關文章:
Orleans介紹
Orleans安裝
Orleans解決并發之痛(一):單線程
Orleans配置---持久化
Orleans解決并發之痛(五):Web API
Orleans解決并發之痛(四):Streams
Orleans解決并發之痛(三):集群
Orleans解決并發之痛(二):Grain狀態
Orleans的集群構建
Orleans簡單配置
原文:?http://www.cnblogs.com/zd1994/p/8087227.html?
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com
總結
- 上一篇: Orleans安装
- 下一篇: .NET Core 已经实现了PHP J