了解 Orleansgrain 的单线程性质

Avi*_*mar 0 c# actor async-await orleans .net-core

我有以下来自奥尔良的客户和谷物的代码片段。(虽然在 Orleans 中推荐的开发方式是等待任务,但以下代码在某些时候并不等待,纯粹是出于实验目的)

// client code
while(true)
{
    Console.WriteLine("Client giving another request");   
    double temperature = random.NextDouble() * 40;   
    var grain = client.GetGrain<ITemperatureSensorGrain>(500);
    Task t = sensor.SubmitTemperatureAsync((float)temperature);
    Console.WriteLine("Client Task Status - "+t.Status);
    await Task.Delay(5000);
}

// ITemperatureSensorGrain code
public async Task SubmitTemperatureAsync(float temperature)
{
   long grainId = this.GetPrimaryKeyLong();
   Console.WriteLine($"{grainId} outer received temperature: {temperature}");

   Task x = SubmitTemp(temperature); // SubmitTemp() is another function in the same grain
   x.Ignore();
   Console.WriteLine($"{grainId} outer received temperature: {temperature} exiting");
}

public async Task SubmitTemp(float temp)
{
    for(int i=0; i<1000; i++)
    {
       Console.WriteLine($"Internal function getting awaiting task {i}");
       await Task.Delay(1000);
    }
}
Run Code Online (Sandbox Code Playgroud)

当我运行上面的代码时,输​​出如下:

Client giving another request
Client Task Status - WaitingForActivation
500 outer received temperature: 23.79668
Internal function getting awaiting task 0
500 outer received temperature: 23.79668 exiting
Internal function getting awaiting task 1
Internal function getting awaiting task 2
Internal function getting awaiting task 3
Internal function getting awaiting task 4
Client giving another request
Client Task Status - WaitingForActivation
500 outer received temperature: 39.0514
Internal function getting awaiting task 0  <------- from second call to SubmitTemp
500 outer received temperature: 39.0514 exiting
Internal function getting awaiting task 5  <------- from first call to SubmitTemp
Internal function getting awaiting task 1
Internal function getting awaiting task 6
Internal function getting awaiting task 2
Internal function getting awaiting task 7
Internal function getting awaiting task 3
Internal function getting awaiting task 8
Internal function getting awaiting task 4
Internal function getting awaiting task 9
Run Code Online (Sandbox Code Playgroud)

从普通 .Net 应用程序的角度来看,输出是有意义的。如果我可以从这篇 stackoverflow 帖子中获得帮助,那么这里发生的事情是:

  1. 客户拨打电话ITemperatureSendorGrain并继续进行。当await命中时,客户端线程将返回到线程池。
  2. SubmitTemperatureAsync接收请求并调用本地异步函数SubmitTemp
  3. SubmitTemp打印对应于 i=0 的语句,然后点击等待。Await 导致其余部分for loop被安排为可等待 (Task.Delay) 的延续,并且控制权返回到调用函数SubmitTemperatureAsync。这里需要注意的是,当线程在SubmitTemp函数中遇到await时,并不会返回线程池。线程控制实际上返回给调用函数SubmitTemperatureAsync。因此,turn按照 Orleans 文档中的定义, a 会在顶级方法遇到等待时结束。当一轮结束时,线程返回到线程池。
  4. 调用函数不会等待任务完成并退出。
  5. 当awaitableSubmitTemp在1秒后返回时,它从线程池中获取一个线程并在其上调度其余的线程for loop
  6. 当客户端代码中的可等待返回时,它会再次调用相同的grain,并for loop计划与第二次调用相对应的另一轮SubmitTemp

我的第一个问题是我是否正确描述了代码中发生的情况,特别是当在函数中点击await时,线程没有返回到线程池SubmitTemp


根据grain的单线程特性,任何时候只有一个线程会执行grain的代码。此外,一旦对grain的请求开始执行,它将在下一个请求被处理之前完全完成(chunk based execution在奥尔良文档中称为)。从高层次上来说,上述代码确实如此,因为SubmitTemperatureAsync只有当当前调用该方法退出时,才会发生下一次调用。

然而,SubmitTemp实际上是 的子功能SubmitTemperatureAsync。尽管SubmitTemperatureAsync已退出,SubmitTemp但仍在执行,并且在执行此操作时,Orleans 允许另一个调用来SubmitTemperatureAsync执行。我的第二个问题是否违反了奥尔良谷物的单线程性质?


考虑到SubmitTempfor loop需要访问grain类的一些数据成员。因此,ExecutionContext当遇到await时,将被捕获,当Task.Delay(1000)返回时,捕获的将被传递给线程上ExecutionContext剩余部分的调度。for loop因为ExecutionContext已通过,所以for loop尽管在不同的线程上运行,剩余部分仍将能够访问数据成员。这是任何普通 .Net 异步应用程序中都会发生的事情。

我的第三个问题是关于SynchronizationContext. 我在 Orleans 存储库中进行了粗略搜索,但找不到 的任何实现SynchronizationContext.Post(),这使我相信不需要SynchronizationContext运行 Orleans 方法。谁能证实这一点吗?如果这不是真的,并且 a 是SynchronizationContext必需的,那么并行执行 的各种调用SubmitTemp(如上面的代码所示)是否会冒以死锁结束的风险(如果有人坚持并不这样做SynchronizationContext)不释放它)?

Reu*_*ond 6

问题 1:所描述的执行流程是否准确地表示了正在发生的情况?

在我看来,您的描述大致正确,但这里有一些要点:

  • 是否有线程池是一个实现细节。
  • “轮次”是激活的 上计划的工作的每个同步部分TaskScheduler
  • 因此,每当执行必须返回到 时,一轮就会结束TaskScheduler
  • 这可能是因为await未同步完成的命中,或者用户await根本没有使用并且正在使用ContinueWith自定义等待项进行编程。
  • 回合可以通过非顶级方法结束,例如,如果将代码更改为await SubmitTemp(x)来代替.Ignoring()it,则回合将在Task.Delay(...)内部被击中时结束SubmitTemp(x)

问题 2:示例程序是否违反了单线程保证?

不,在给定时间只有一个线程执行grain的代码。然而,该“线程”必须在激活上安排的各种任务之间分配时间TaskScheduler。即,永远不会有这样的情况:您挂起进程并发现两个线程同时执行您的grain 代码。

就运行时而言,当Task从顶级方法返回(或其他可等待类型)完成时,消息的处理就会结束。在此之前,不会安排在您激活时执行任何新消息。从您的方法生成的后台任务始终允许与其他任务交错。

.NET 允许子任务附加到其父任务。在这种情况下,父任务仅在所有子任务完成后才完成。然而,这不是默认行为,通常建议您避免选择此行为(例如,通过传递TaskCreationOptions.AttachedToParentTask.Factory.StartNew)。

如果您确实使用了该行为(请不要使用),那么您将在第一次调用时看到SubmitTemp()无限期的激活循环,​​并且不会再处理任何消息。

问题3:奥尔良使用吗SynchronizationContext

奥尔良使用SynchronizationContext. 相反,它使用自定义TaskScheduler实现。看ActivationTaskScheduler.cs。每个激活都有其自己的ActivationTaskScheduler,并且所有消息都是使用该调度程序的调度程序。

关于后续问题,针对Task激活进行调度的实例(每个实例代表一项同步工作)被插入到同一队列中,因此允许它们交错,但ActivationTaskScheduler一次只能由一个线程执行。