在奥尔良,我有一个客户的以下代码片段。 (尽管在奥尔良推荐的开发方式是等待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寻求帮助,那么这里发生的是:
ITemperatureSendorGrain
,然后继续进行。当await
被点击时,客户端线程返回到线程池。 SubmitTemperatureAsync
收到请求并调用本地异步函数SubmitTemp
。SubmitTemp
打印对应于i = 0的语句,此后它命中。等待将for loop
的其余部分安排为可等待(Task.Delay)的继续,然后控件返回到调用函数SubmitTemperatureAsync
。请注意,在SubmitTemp
函数中遇到等待时,线程不会返回到线程池。线程控制实际上返回到调用函数SubmitTemperatureAsync
。因此,在奥尔良文档中定义的turn
在顶级方法遇到等待时结束。转弯结束时,线程返回线程池。 SubmitTemp
中的等待时间在1秒后返回时,它将从线程池中获取一个线程,并在其上调度其余for loop
。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
而没有释放它)?
答案 0 :(得分:4)
您的描述对我来说大致正确,但是这里有一些要点:
TaskScheduler
上安排的每个同步工作部分。TaskScheduler
时,转弯结束。await
,或者用户根本没有使用await
,而是使用ContinueWith
或自定义的awaitables进行了编程。 await SubmitTemp(x)
而不是.Ignoring()
,则转弯将在Task.Delay(...)
时结束在SubmitTemp(x)
内部被击中。不,在给定的时间只有一个线程执行谷物的代码。 但是,该“线程”必须在激活TaskScheduler
上安排的各种任务之间分配时间。也就是说,永远不会有永远暂停进程并发现两个线程正在同时执行grain的代码的时间。
就运行时而言,从顶级方法返回的Task
(或其他可等待的类型)完成时,对消息的处理结束。在此之前,不会安排任何新消息在激活时执行。从您的方法产生的后台任务总是被允许与其他任务交错。
.NET允许将子任务附加到其父任务。在这种情况下,父任务仅在所有子任务完成时才完成。但是,这不是默认行为,通常建议您避免选择加入此行为(例如,将TaskCreationOptions.AttachedToParent
传递给Task.Factory.StartNew
)。
如果您确实使用了这种行为(请不要使用),那么您会在第一次无限期致电SubmitTemp()
时看到激活循环,并且将不再处理任何消息。
SynchronizationContext
?奥尔良不使用SynchronizationContext
。相反,它使用自定义TaskScheduler
实现。参见ActivationTaskScheduler.cs
。每次激活都有自己的ActivationTaskScheduler
,所有消息都是使用该计划程序的计划程序。
关于后续问题,针对激活计划的Task
实例(每个实例代表一个同步的工作)被插入到同一队列中,因此可以进行交织,但是{ {1}}一次只能由一个线程执行。
答案 1 :(得分:2)
我知道这是一个人为设计的代码段,旨在探索Orleans运行时的执行保证。我有点担心有人可能会读到这,并且误认为这是应如何实施谷物方法的推荐模式。
这就是为什么我要强调的是,建议的编写粒度代码的方法是等待调用堆栈中的每个Task
。在上面的代码中,这意味着等待grain方法中的x
和客户端代码中的t
。默认情况下,grain是不可重入的,这将阻止客户端执行第二个调用以在第一个执行完成之前开始执行。或者,可以选择将谷物类别标记为[Reentrant]
并允许交织第二个呼叫。这将比后台循环更加清晰明了,并使错误处理成为可能。