准备工作

推荐使用.NET Standard来创建项目。Grain接口和Grain类可以定义在工程里,也可以定义在多个工程里,以便更好地区分接口和实现。不过无论如何,都需要引用这两个NuGet包:Microsoft.Orleans.Core.Abstractions和Microsoft.Orleans.CodeGenerator.MSBuild。
关于准备工作更全面的文档,见教程与样例

Grain接口和类

Grain之间的交互和被外部调用,都使用的是定义在接口中的方法。一个Grain类可以实现一个或多个事先定义好的Grain接口。Grain接口的所有方法的返回值都必须为TaskTask<T>ValueTask<T>
下面摘录一个示例:

  1. //an example of a Grain Interface
  2. public interface IPlayerGrain : IGrainWithGuidKey
  3. {
  4. Task<IGameGrain> GetCurrentGame();
  5. Task JoinGame(IGameGrain game);
  6. Task LeaveGame(IGameGrain game);
  7. }
  8. //an example of a Grain class implementing a Grain Interface
  9. public class PlayerGrain : Grain, IPlayerGrain
  10. {
  11. private IGameGrain currentGame;
  12. // Game the player is currently in. May be null.
  13. public Task<IGameGrain> GetCurrentGame()
  14. {
  15. return Task.FromResult(currentGame);
  16. }
  17. // Game grain calls this method to notify that the player has joined the game.
  18. public Task JoinGame(IGameGrain game)
  19. {
  20. currentGame = game;
  21. Console.WriteLine(
  22. "Player {0} joined game {1}",
  23. this.GetPrimaryKey(),
  24. game.GetPrimaryKey());
  25. return Task.CompletedTask;
  26. }
  27. // Game grain calls this method to notify that the player has left the game.
  28. public Task LeaveGame(IGameGrain game)
  29. {
  30. currentGame = null;
  31. Console.WriteLine(
  32. "Player {0} left game {1}",
  33. this.GetPrimaryKey(),
  34. game.GetPrimaryKey());
  35. return Task.CompletedTask;
  36. }
  37. }

Grain方法的返回值

如果一个方法可以返回一个类型T的值,在Grain接口中,应该被定义其返回类型为Task<T>。如果Grain方法没有标记async关键字,拿到返回值后可以这么写:

  1. public Task<SomeType> GrainMethod1()
  2. {
  3. ...
  4. return Task.FromResult(<variable or constant with result>);
  5. }

如果一个Grain方法返回值实际上是一个void,在Grain接口中就将其返回值定义为TaskTask表示这个方法是异步执行的,且已经执行完毕。这种情况下如果没有标记async关键字,可以在最后返回Task.CompletedTask

  1. public Task GrainMethod2()
  2. {
  3. ...
  4. return Task.CompletedTask;
  5. }

而如果Grain方法实现上标记了async关键字,就可以直接返回值:

  1. public async Task<SomeType> GrainMethod3()
  2. {
  3. ...
  4. return <variable or constant with result>;
  5. }

“void”方法的情况:

  1. public async Task GrainMethod4()
  2. {
  3. ...
  4. return;
  5. }

如果一个Grain方法异步调用了其他方法并接收了其返回值,并且不需要对这个调用做异常处理,那么就用一个Task类型的变量来接收这个返回值。

  1. public Task<SomeType> GrainMethod5()
  2. {
  3. ...
  4. Task<SomeType> task = CallToAnotherGrain();
  5. return task;
  6. }

类似地,“void”方法的情况:

  1. public Task GrainMethod6()
  2. {
  3. ...
  4. Task task = CallToAsyncAPI();
  5. return task;
  6. }

可以用ValueTask<T>来代替Task<T>

Grain引用

Grain引用是一个代理对象(proxy object),它实现了和对应的Grain类一样的接口(应该是动态代理模式)。它为目标Grain封装了一个逻辑标识(结合类型和唯一的key)。Grain引用的作用就是调用目标Grain。每一个Grain引用对应的是一个单例的Grain,不过可以为同一个Grain创建多个不同的Grain引用(多个Grain引用实例都针对同一个Grain实例做了动态代理的意思吧)。
由于Grain引用相当于目标Grain的逻辑标识,与Grain的物理位置没什么关系,哪怕系统重启,Grain引用依然是可用的。开发者可以像使用其他.NET对象那样使用Grain引用:可以作为参数传给一个方法,作为方法的返回值,等等,甚至可以持久化到存储里。
可以通过往方法GrainFactory.GetGrain<T>(key)里传入标识来获得一个Grain引用,其中T是Grain接口,key是该类型的Grain中一个唯一的key。
接下来演示一下如何获得上层定义了Grain接口IPlayerGrain的Grain引用。
从Grain类内部:

  1. //construct the grain reference of a specific player
  2. IPlayerGrain player = GrainFactory.GetGrain<IPlayerGrain>(playerId);

从Orleans客户端代码:

  1. IPlayerGrain player = client.GetGrain<IPlayerGrain>(playerId);

Grain方法调用

Orleans编程模型基于异步编程
继续利用上一节中获取到的Grain引用,可以这样调用Grain方法:

  1. //Invoking a grain method asynchronously
  2. Task joinGameTask = player.JoinGame(this);
  3. //The await keyword effectively makes the remainder of the method execute asynchronously at a later point (upon completion of the Task being awaited) without blocking the thread.
  4. await joinGameTask;
  5. //The next line will execute later, after joinGameTask is completed.
  6. players.Add(playerId);

可以把多个Task结合(join)起来,结合的意义在于,创建了一个新的Task,当被结合的Task都被异步调用完成后,才可以解析这个(新建的)Task。这个模式在Grain需要启动多个计算过程,且需要等这些计算过程都结束后才能继续接下来的操作时,很有用。例如,一个用来生成web页面的前端Grain可能由很多部分构成,需要调用多次后端逻辑,每一部分的调用都接收一个Task作为结果。那么这个Grain可以等待这些Task的结合(join Task)完成调用以后,再解析这些Task,从而拿到所有组成web页面的所有数据。
示例:

  1. List<Task> tasks = new List<Task>();
  2. Message notification = CreateNewMessage(text);
  3. foreach (ISubscriber subscriber in subscribers)
  4. {
  5. tasks.Add(subscriber.Notify(notification));
  6. }
  7. // WhenAll joins a collection of tasks, and returns a joined Task that will be resolved when all of the individual notification Tasks are resolved.
  8. Task joinedTask = Task.WhenAll(tasks);
  9. await joinedTask;
  10. // Execution of the rest of the method will continue asynchronously after joinedTask is resolve.

虚方法

Grain类可以选择重写以下两个虚方法:

  • OnActivateAsync
  • OnDeactivateAsync

这两个虚方法会在每个该Grain类的实例被激活或者失活的时候,被Orleans运行时调用。通过这种方式,Grain的代码就有能力在Grain实例被初始化和被清除的时候添加新的逻辑。
如果OnActivateAsync方法在执行时抛出异常,Grain实例的激活就会失败。
一旦重写了OnActivateAsync方法,方法里的逻辑就会作为激活Grain实例的一部分逻辑,每次激活时都会被调用,而OnDeactivateAsync方法就没办法保证在所有失活的情况下都会被调用了,例如在服务器故障或其他异常情况下时。因此,应用不应使用OnDeactivateAsync方法来完成一些至关重要的操作,如将Grain状态的修改持久化到数据库。重写OnDeactivateAsync方法应该仅用来完成一些尽力而为(best effort)的操作。