在 Orleans 中与粮食相关的两种调度形式:
- 请求调度:根据 请求调度中讨论的规则调度传入粮食请求的执行。
- 任务计划:计划以 单线程 方式执行的同步代码块。
所有粒度代码在粒度的任务计划程序上执行,这意味着请求也会在粒度的任务计划程序上执行。 即使请求调度规则允许多个请求并发执行,它们也不会并行执行,因为该任务调度程序始终逐个执行任务,并且永远不会并行执行多个任务。
任务计划程序
若要更好地了解日程安排,请考虑以下粒度 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");
}
}
此方法执行时,方法正文分两部分执行:
- 第一个部分是
_logger.LogInformation(...)
调用以及对Task.Delay(1_000)
的调用。 - 第二个部分是
_logger.LogInformation(...)
调用。
在Task.Delay(1_000)
调用完成之前,第二个任务不会在粒度任务计划程序中安排。 此时,它会安排谷粒方法的 延续 。
下面是一个图形表示,展示如何将一个请求计划并执行为两个任务。
上述说明不特定于 Orleans;它描述任务计划在 .NET 中的工作原理。 C# 编译器将异步方法转换为异步状态机,并在离散步骤中通过此状态机执行。 每个步骤按当前 TaskScheduler (通过 TaskScheduler.Current默认访问 TaskScheduler.Default)或当前 SynchronizationContext步骤进行计划。 如果使用TaskScheduler
,则该方法中的每个步骤都表示一个Task
实例,该实例被传递给TaskScheduler
。 因此,.NET 中的 Task
可以表示两种概念:
- 可以等待的异步操作 上述
DelayExecution()
方法的执行由一个可等待的Task
表示。 - 同步的工作单元。 上述方法中的每个
DelayExecution()
所处阶段均由一个Task
来表示。
使用 TaskScheduler.Default
时,延续任务会直接安排到 .NET ThreadPool 上,并且不会被包装在 Task
对象中。 实例中的 Task
延续的包装是透明的,因此开发人员很少需要了解这些实现详细信息。
Orleans 中的任务计划
每个粒度激活都有自己的 TaskScheduler
实例,负责强制实施粒度的单 线程 执行模型。 在内部,此 TaskScheduler
是通过 ActivationTaskScheduler
和 WorkItemGroup
实现的。
WorkItemGroup
将排队的任务保存在 Queue<T> 中(其中 T
在内部是一个 Task
),并实现 IThreadPoolWorkItem。 若要执行每个当前排队的 Task
,WorkItemGroup
将在 .NET 上计划自身ThreadPool
。 当 .NET ThreadPool
调用 WorkItemGroup
的 IThreadPoolWorkItem.Execute()
方法时,WorkItemGroup
会逐个执行排列的 Task
实例。
每个任务都有一个调度器,该调度器通过在 .NET ThreadPool
上进行自我调度来执行:
每个计划程序包含一个任务队列:
.NET ThreadPool
执行其中排队的每个工作项。 这包括 任务调度器 以及其他工作项,例如通过 Task.Run(...)
:
注释
一个粒子的调度器每次只能在一个线程上执行,但并不是总在同一个线程上执行。 每次执行 Grain 的调度程序时,.NET ThreadPool
都可以自由使用不同的线程。 粒子的调度程序确保它一次只在一个线程上执行,实现粒子的单线程执行模型。