计划概述

Orleans 中有两种与 grain 相关的计划形式:

  1. 请求计划:将传入的 grain 调用计划为根据请求计划中所述的计划规则执行。
  2. 任务计划:将同步代码块计划为以单线程方式执行

所有 grain 代码都在 grain 的任务计划程序中执行,这意味着,请求也在 grain 的任务计划程序中执行。 即使请求计划规则允许多个请求并发执行,这些请求也不会并行执行,因为 grain 的任务计划程序始终逐个执行任务,因此永远不会并行执行多个任务。

任务计划程序

为了更好地了解计划,请考虑以下 grain MyGrain,它具有一个名为 DelayExecution() 的方法,该方法记录一条消息,等待一段时间,然后在返回之前记录另一条消息。

public interface IMyGrain : IGrain
{
    Task DelayExecution();
}

public class MyGrain : Grain, IMyGrain
{
    private readonly ILogger<MyGrain> _logger;

    public MyGrain(ILogger<MyGrain> logger) => _logger = logger;

    public async Task DelayExecution()
    {
        _logger.LogInformation("Executing first task");

        await Task.Delay(1_000);

        _logger.LogInformation("Executing second task");
    }
}

当此方法执行时,方法主体将分两个部分执行:

  1. 第一个部分是 _logger.LogInformation(...) 调用以及对 Task.Delay(1_000) 的调用。
  2. 第二个部分是 _logger.LogInformation(...) 调用。

Task.Delay(1_000) 调用完成之前,第二个任务不会在 grain 的任务计划程序中进行计划,此时它会计划 grain 方法的延续。

下面是如何计划请求并将其作为两个任务执行的图形表示形式:

Two-Task-based request execution example.

以上说明并非特定于 Orleans,而是 .NET 中任务计划的工作原理:编译器将 C# 中的异步方法转换为异步状态机,执行通过异步状态机以离散步骤的形式不断进行。 每个步骤在当前的 TaskScheduler(通过 TaskScheduler.Current 访问,默认为 TaskScheduler.Default)或当前的 SynchronizationContext 中计划。 如果使用 TaskScheduler,则方法中的每个步骤由传递给该 TaskSchedulerTask 实例表示。 因此,.NET 中的 Task 可以表示两种概念:

  1. 可以等待的异步操作。 上述 DelayExecution() 方法的执行由可以等待的 Task 表示。
  2. 在同步工作块中,上述 DelayExecution() 方法中的每个阶段由 Task 表示。

使用 TaskScheduler.Default 时,延续将直接计划到 .NET ThreadPool,而不是包装在 Task 对象中。 Task 实例中的延续包装以透明方式发生,因此开发人员基本上不需要了解这些实现细节。

Orleans 中的任务计划

每个 grain 激活都有自身的 TaskScheduler 实例,该实例负责强制实施 grain 的单线程执行模型。 在内部,此 TaskScheduler 是通过 ActivationTaskSchedulerWorkItemGroup 实现的。 WorkItemGroup 将排队的任务保留在 Queue<T> 中,其中 T 在内部是 Task 并实现 IThreadPoolWorkItem。 若要执行每个当前排队的 TaskWorkItemGroup 将在 .NET ThreadPool 上计划自身。 当 .NET ThreadPool 调用 WorkItemGroupIThreadPoolWorkItem.Execute() 方法时,WorkItemGroup 将逐个执行排队的 Task 实例。

每个 grain 都有一个计划程序,该计划程序通过在 .NET ThreadPool 上计划自身来执行:

Orleans grains scheduling themselves on the .NET ThreadPool.

每个计划程序包含一个任务队列:

Scheduler queue of scheduled tasks.

.NET ThreadPool 执行其中排队的每个工作项。 这包括 grain 计划程序以及其他工作项,例如通过 Task.Run(...) 计划的工作项:

Visualization of the all schedulers running in the .NET ThreadPool.

注意

grain 的计划程序每次只能在一个线程上执行,但它并不始终在同一个线程上执行。 每次执行 grain 的计划程序时,.NET ThreadPool 都可以任意使用不同的线程。 grain 的计划程序负责确保每次只在一个线程上执行,这也是 grain 的单线程执行模型的实现方式。