开发 grain

在编写用于实现 grain 类的代码之前,需要创建一个面向 .NET Standard或 .Net Core(首选)或者 .NET Framework 4.6.1 或更高版本(如果由于依赖关系的原因而无法使用 .NET Standard 或 .NET Core)的新类库项目。 grain 接口和 grain 类可以在同一个类库项目中定义,也可以在两个不同的项目中定义,以便更好地将接口与实现分离。 无论采取哪种定义方式,项目都需要引用 Microsoft.Orleans.Core.AbstractionsMicrosoft.Orleans.CodeGenerator.MSBuild NuGet 包。

有关更详尽的说明,请参阅教程一 - Orleans 基础知识中的项目设置部分。

grain 接口和类

grain 会彼此交互;可以通过调用声明为相应 grain 接口的一部分的方法从外部调用 grain。 grain 类实现一个或多个先前声明的 grain 接口。 grain 接口的所有方法都必须返回 Task(对于 void 方法)、Task<TResult>ValueTask<TResult>(对于返回 T 类型的值的方法)。

下面是 Orleans 版本 1.5 Presence Service 示例的摘录:

public interface IPlayerGrain : IGrainWithGuidKey
{
    Task<IGameGrain> GetCurrentGame();
    Task JoinGame(IGameGrain game);
    Task LeaveGame(IGameGrain game);
}

public class PlayerGrain : Grain, IPlayerGrain
{
    private IGameGrain _currentGame;

    // Game the player is currently in. May be null.
    public Task<IGameGrain> GetCurrentGame()
    {
       return Task.FromResult(_currentGame);
    }

    // Game grain calls this method to notify that the player has joined the game.
    public Task JoinGame(IGameGrain game)
    {
       _currentGame = game;

       Console.WriteLine(
           $"Player {GetPrimaryKey()} joined game {game.GetPrimaryKey()}");

       return Task.CompletedTask;
    }

   // Game grain calls this method to notify that the player has left the game.
   public Task LeaveGame(IGameGrain game)
   {
       _currentGame = null;

       Console.WriteLine(
           $"Player {GetPrimaryKey()} left game {game.GetPrimaryKey()}");

       return Task.CompletedTask;
   }
}

从 grain 方法返回值

返回 T 类型的值的 grain 方法在 grain 接口中定义为返回 Task<T>。 对于未标记关键字 async 的 grain 方法,当返回值可用时,通常会通过以下语句返回该值:

public Task<SomeType> GrainMethod1()
{
    return Task.FromResult(GetSomeType());
}

不返回值的 grain 方法(实际上是 void 方法)在 grain 接口中定义为返回 Task。 返回的 Task 指示方法的异步执行和完成。 对于未标记关键字 async 的 grain 方法,当某个“void”方法完成执行时,它需要返回特殊值 Task.CompletedTask

public Task GrainMethod2()
{
    return Task.CompletedTask;
}

标记为 async 的 grain 方法直接返回值:

public async Task<SomeType> GrainMethod3()
{
    return await GetSomeTypeAsync();
}

标记为 async 且不返回任何值的 void grain 方法仅在其执行结束时返回:

public async Task GrainMethod4()
{
    return;
}

如果某个 grain 方法从另一个异步方法调用接收返回值(无论返回值是否发送到 grain),并且不需要对该调用执行错误处理,则它可以简单地返回它从该异步调用接收的 Task

public Task<SomeType> GrainMethod5()
{
    Task<SomeType> task = CallToAnotherGrain();

    return task;
}

同样,void grain 方法可以返回另一个调用返回给它的 Task,而无需等待。

public Task GrainMethod6()
{
    Task task = CallToAsyncAPI();
    return task;
}

可以使用 ValueTask<T> 而不是 Task<T>

Grain 引用

Grain 引用是一个代理对象,它实现与相应 grain 类相同的 grain 接口。 它封装目标 grain 的逻辑标识(类型和唯一键)。 grain 引用用于向目标 grain 发出调用。 每个 grain 引用指向单个 grain(grain 类的单个实例),但用户可以创建对同一个 grain 的多个独立引用。

由于 grain 引用表示目标 grain 的逻辑标识,因此它独立于 grain 的物理位置,即使在系统完全重启后也仍然有效。 开发人员可以像使用任何其他 .NET 对象一样使用 grain 引用。 可将它传递给方法、用作方法返回值,等等,甚至可将它保存到持久性存储中。

可以通过将 grain 的标识传递给 IGrainFactory.GetGrain<TGrainInterface>(Type, Guid) 方法来获取 grain 引用,其中 T 是 grain 接口,key 是 grain 在类型中的唯一键。

下面是如何获取上面定义的 IPlayerGrain 接口的 grain 引用的示例。

从 grain 类内部:

IPlayerGrain player = GrainFactory.GetGrain<IPlayerGrain>(playerId);

从 Orleans 客户端代码。

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

grain 方法调用

Orleans 编程模型基于异步编程。 沿用上一个示例中的 grain 引用,下面演示了如何执行 grain 方法调用:

// Invoking a grain method asynchronously
Task joinGameTask = player.JoinGame(this);

// 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.
await joinGameTask;

// The next line will execute later, after joinGameTask has completed.
players.Add(playerId);

可以联接两个或更多个 Tasks;联接操作会新建一个在其所有 Task 构成部分完成后解析的 Task。 当 grain 需要启动多个计算并等待所有计算完成后再继续时,此模式非常有用。 例如,生成由许多部件组成的网页的前端 grain 可以发出多次后端调用(针对每个部件发出一次),并接收每个结果的 Task。 然后,grain 将等待联接所有这些 Tasks;当解析了联接 Task 时,各个 Task 已完成,并已收到设置网页格式所需的所有数据。

例如:

List<Task> tasks = new List<Task>();
Message notification = CreateNewMessage(text);

foreach (ISubscriber subscriber in subscribers)
{
    tasks.Add(subscriber.Notify(notification));
}

// WhenAll joins a collection of tasks, and returns a joined
// Task that will be resolved when all of the individual notification Tasks are resolved.
Task joinedTask = Task.WhenAll(tasks);

await joinedTask;

// Execution of the rest of the method will continue
// asynchronously after joinedTask is resolve.

虚拟方法

grain 类可以选择性地重写 OnActivateAsyncOnDeactivateAsync 虚拟方法;在激活和取消激活类的每个 grain 时,Orleans 运行时会调用这些方法。 这样,grain 代码便有机会执行额外的初始化和清理操作。 OnActivateAsync 引发的异常会使激活进程失败。 虽然 OnActivateAsync(如果已重写)始终作为 grain 激活进程的一部分调用,但不能保证在所有情况下都会调用 OnDeactivateAsync,例如,在发生服务器故障或其他异常事件的情况下就不会调用。 因此,应用程序不应依赖 OnDeactivateAsync 来执行关键操作,例如持久保存状态更改。 应用程序应该仅将它用于尽力而为的操作。