了解奥尔良谷物的单线程性质



我有以下奥尔良的客户端和谷物的代码片段(尽管在奥尔良推荐的开发方式是等待任务,但以下代码在某些时候并不是纯粹为了实验目的而等待的)

// 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);
}
}

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

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

从法线的角度来看,输出是有意义的。Net应用程序。如果我能从这篇stackoverflow帖子中得到帮助,这里发生的事情是:

  1. 客户端调用ITemperatureSendorGrain并继续进行。当await命中时,客户端线程将返回到线程池
  2. CCD_ 3接收该请求并调用本地异步函数CCD_
  3. SubmitTemp打印与i=0相对应的语句,在该语句之后它将命中等待。Await使for loop的其余部分被调度为Awaable(Task.Delay)的延续,并且控制返回到调用函数SubmitTemperatureAsync。请注意,当线程在SubmitTemp函数中遇到等待时,它不会返回到线程池。线程控制实际上被返回到调用函数SubmitTemperatureAsync。因此,正如Orleans文档中所定义的,turn在顶级方法遇到等待时结束。当循环结束时,线程将返回到线程池
  4. 调用函数不等待任务完成并退出
  5. SubmitTemp中的awaitable在1s后返回时,它从线程池中获取一个线程,并在其上调度其余的for loop
  6. 当awaitable in client代码返回时,它对同一粒度进行另一次调用,并且对应于对SubmitTemp的第二次调用来调度另一轮for loop

我的第一个问题是我是否正确地描述了代码中发生的事情,特别是关于在函数SubmitTemp中命中wait时线程没有返回到线程池的情况。


根据晶粒的单线程性质,在任何时候只有一个线程执行晶粒的代码。此外,一旦对一个晶粒的请求开始执行,它将在下一个请求被接受之前完全完成(在奥尔良文档中称为chunk based execution)。从高层来看,以上代码也是如此,因为只有当对方法的当前调用退出时,才会对SubmitTemperatureAsync进行下一次调用。

SubmitTemp实际上是SubmitTemperatureAsync的一个子函数。尽管SubmitTemperatureAsync退出,但SubmitTemp仍在执行,并且在执行时,Orleans允许执行对SubmitTemperatureAsync的另一个调用。这是否违反了奥尔良谷物的单线程性质?我的第二个问题是?


考虑SubmitTemp在其for loop中需要访问grain类的一些数据成员。因此,当遇到等待时,ExecutionContext将被捕获,并且当Task.Delay(1000)返回时,捕获的ExecutionContext将被传递给线程上剩余for loop的调度。因为传递了ExecutionContext,所以剩余的SubmitTemperatureAsync0将能够访问数据成员,尽管运行在不同的线程上。这是任何正常情况下都会发生的事情。Net异步应用程序。

我的第三个问题是关于SynchronizationContext的。我在奥尔良存储库中进行了粗略的搜索,但找不到任何SynchronizationContext.Post()的实现,这让我相信运行奥尔良方法不需要SynchronizationContext。有人能证实吗?如果这不是真的,并且需要SynchronizationContext,那么SubmitTemp的各种调用的并行执行(如上面的代码所示)难道不会有以死锁告终的风险吗(如果有人抓住SynchronizationContext而不释放它)?

问题1:所描述的执行流是正在发生的事情的准确表示吗

在我看来,你的描述大致正确,但这里有一些更精细的地方:

  • 是否有线程池是一个实现细节
  • "转弯"是在激活的TaskScheduler上安排的每个同步工作部分
  • 因此,每当执行必须返回给TaskScheduler时,一个回合就结束了
  • 这可能是因为await未同步完成,或者用户根本没有使用SubmitTemp0,而是使用ContinueWith或自定义awaitables进行编程
  • 一个回合可以从非顶级方法结束,例如,如果代码改为await SubmitTemp(x)而不是.Ignoring(),那么当Task.Delay(...)SubmitTemp(x)内被击中时,回合将结束

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

不,在给定的时间里,只有一个线程在执行grain的代码但是,"线程"必须在激活的TaskScheduler上调度的各种任务之间分配时间。也就是说,永远不会有挂起进程并发现两个线程同时执行您的grain代码的情况。

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

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

如果你确实利用了这种行为(请不要),那么你会在第一次调用SubmitTemp()时无限期地看到你的激活循环,并且不会处理更多的消息。

问题3:奥尔良使用SynchronizationContext

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

关于接下来的问题,针对激活而调度的Task实例(每个实例代表一个同步工作)被插入到同一队列中,因此它们被允许交织,但ActivationTaskScheduler一次仅由一个线程执行。

我知道这是一个人为的代码片段,旨在探索奥尔良运行时的执行保证。我有点担心,有人可能读到这篇文章,并误解这是应该如何实现细粒度方法的推荐模式。

这就是为什么我想强调的是,推荐的编写细粒度代码的方法是等待调用堆栈中的每个Task。在上面的代码中,这意味着等待粒度方法中的x和客户端代码中的t。默认情况下,粒度是不可重入的,这将阻止客户端在第一个调用执行完成之前开始执行第二个调用。或者可以选择将粒度类标记为[Reentrant],并允许交织第二个调用。这将比后台循环更加清晰和明确,并使错误处理成为可能。

相关内容

  • 没有找到相关文章

最新更新