我有以下奥尔良的客户端和谷物的代码片段(尽管在奥尔良推荐的开发方式是等待任务,但以下代码在某些时候并不是纯粹为了实验目的而等待的)
// 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帖子中得到帮助,这里发生的事情是:
- 客户端调用
ITemperatureSendorGrain
并继续进行。当await
命中时,客户端线程将返回到线程池 - CCD_ 3接收该请求并调用本地异步函数CCD_
SubmitTemp
打印与i=0相对应的语句,在该语句之后它将命中等待。Await使for loop
的其余部分被调度为Awaable(Task.Delay)的延续,并且控制返回到调用函数SubmitTemperatureAsync
。请注意,当线程在SubmitTemp
函数中遇到等待时,它不会返回到线程池。线程控制实际上被返回到调用函数SubmitTemperatureAsync
。因此,正如Orleans文档中所定义的,turn
在顶级方法遇到等待时结束。当循环结束时,线程将返回到线程池- 调用函数不等待任务完成并退出
- 当
SubmitTemp
中的awaitable在1s后返回时,它从线程池中获取一个线程,并在其上调度其余的for loop
- 当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
,所以剩余的SubmitTemperatureAsync
0将能够访问数据成员,尽管运行在不同的线程上。这是任何正常情况下都会发生的事情。Net异步应用程序。
我的第三个问题是关于SynchronizationContext
的。我在奥尔良存储库中进行了粗略的搜索,但找不到任何SynchronizationContext.Post()
的实现,这让我相信运行奥尔良方法不需要SynchronizationContext
。有人能证实吗?如果这不是真的,并且需要SynchronizationContext
,那么SubmitTemp
的各种调用的并行执行(如上面的代码所示)难道不会有以死锁告终的风险吗(如果有人抓住SynchronizationContext
而不释放它)?
问题1:所描述的执行流是正在发生的事情的准确表示吗
在我看来,你的描述大致正确,但这里有一些更精细的地方:
- 是否有线程池是一个实现细节
- "转弯"是在激活的
TaskScheduler
上安排的每个同步工作部分 - 因此,每当执行必须返回给
TaskScheduler
时,一个回合就结束了 - 这可能是因为
await
未同步完成,或者用户根本没有使用SubmitTemp
0,而是使用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]
,并允许交织第二个调用。这将比后台循环更加清晰和明确,并使错误处理成为可能。