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

时间:2018-11-28 22:55:37

标签: c# .net-core async-await actor orleans

在奥尔良,我有一个客户的以下代码片段。 (尽管在奥尔良推荐的开发方式是等待Tasks,但以下代码并非仅出于实验目的而等待)

// 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应用程序的角度来看,输出是有意义的。如果我可以从this stackoverflow post寻求帮助,那么这里发生的是:

  1. 客户致电ITemperatureSendorGrain,然后继续进行。当await被点击时,客户端线程返回到线程池。
  2. SubmitTemperatureAsync收到请求并调用本地异步函数SubmitTemp
  3. SubmitTemp打印对应于i = 0的语句,此后它命中。等待将for loop的其余部分安排为可等待(Task.Delay)的继续,然后控件返回到调用函数SubmitTemperatureAsync。请注意,在SubmitTemp函数中遇到等待时,线程不会返回到线程池。线程控制实际上返回到调用函数SubmitTemperatureAsync。因此,在奥尔良文档中定义的turn在顶级方法遇到等待时结束。转弯结束时,线程返回线程池。
  4. 调用函数不等待任务完成并退出。
  5. SubmitTemp中的等待时间在1秒后返回时,它将从线程池中获取一个线程,并在其上调度其余for loop
  6. 当客户端代码中的waiting返回时,它将再次调用相同的粒度,并安排了与for loop的第二次调用相对应的另一轮SubmitTemp

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


根据谷物的单线程性质,在任何时候,只有一个线程将执行谷物的代码。另外,一旦开始执行对谷物的请求,该请求将在下一个请求被接受之前完全完成(在奥尔良文档中称为chunk based execution)。在较高的层次上,这对于上面的代码是正确的,因为对SubmitTemperatureAsync的下一次调用仅在当前对方法的调用退出时才会发生。

但是,SubmitTemp实际上是SubmitTemperatureAsync的子功能。尽管SubmitTemperatureAsync已退出,但SubmitTemp仍在执行,并且这样做时,奥尔良允许再次调用SubmitTemperatureAsync来执行。这是否违反了奥尔良谷物的单线程性质我的第二个问题


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

我的第三个问题是关于SynchronizationContext的。我在Orleans信息库中进行了粗略的搜索,但是找不到SynchronizationContext.Post()的任何实现,这使我相信运行Orleans方法不需要SynchronizationContext。有人可以确认吗?如果这不是真的,并且需要SynchronizationContext,则不会并行执行SubmitTemp的各种调用(如上面的代码所示),冒着陷入死锁的危险(如果有人坚持使用SynchronizationContext而没有释放它)?

2 个答案:

答案 0 :(得分:4)

问题1:所描述的执行流程是对所发生情况的准确表示吗?

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

  • 是否有线程池是实现的详细信息。
  • “转弯”是在激活TaskScheduler上安排的每个同步工作部分。
  • 因此,每当必须将执行退回TaskScheduler时,转弯结束。
  • 这可能是因为未同步完成命中await,或者用户根本没有使用await,而是使用ContinueWith或自定义的awaitables进行了编程。
  • 转弯可以通过非顶级方法结束,例如,如果代码更改为await SubmitTemp(x)而不是.Ignoring(),则转弯将在Task.Delay(...)时结束在SubmitTemp(x)内部被击中。

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

不,在给定的时间只有一个线程执行谷物的代码。 但是,该“线程”必须在激活TaskScheduler上安排的各种任务之间分配时间。也就是说,永远不会有永远暂停进程并发现两个线程正在同时执行grain的代码的时间。

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

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

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

问题3:奥尔良是否使用SynchronizationContext

奥尔良不使用SynchronizationContext。相反,它使用自定义TaskScheduler实现。参见ActivationTaskScheduler.cs。每次激活都有自己的ActivationTaskScheduler,所有消息都是使用该计划程序的计划程序。

关于后续问题,针对激活计划的Task实例(每个实例代表一个同步的工作)被插入到同一队列中,因此可以进行交织,但是{ {1}}一次只能由一个线程执行。

答案 1 :(得分:2)

我知道这是一个人为设计的代码段,旨在探索Orleans运行时的执行保证。我有点担心有人可能会读到这,并且误认为这是应如何实施谷物方法的推荐模式。

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

相关问题