【问题标题】:Difference between method call in normal C# and Orleans普通 C# 和 Orleans 中的方法调用之间的区别
【发布时间】:2018-08-03 01:23:22
【问题描述】:

我在 localHost 集群模式下运行 Orleans,目前有 1 个grain 和一个客户端。

// client code
for (int i = 0; i <num_scan; ++i)                    
{
    Console.WriteLine("client " + i);
    // the below call should have returned when first await is hit in foo()
    // but it doesn't work like that
    grain.foo(i);          
}

// grain code
async Task foo(int i)
{
     Console.WriteLine("grain "+i);
     await Task.Delay(2000);
}

输出如下:

client 0
client 1
client 2
client 3
client 4
client 5
client 6
grain 0
client 7
client 8
client 9
client 10
grain 8
grain 7
.
.

normal C# 中,异步函数仅在遇到await 时才返回。在这种情况下,粮食产量应该是连续的。正如我们在上面看到的,谷物输出是乱序的。因此,任务在到达await 语句之前返回。 我的问题是奥尔良中的方法调用和普通 C# 有什么区别。

我看到this post问了一个类似的问题,回复表明这两种方法调用的情况是不同的,因为我们在奥尔良调用了一个接口。 我想知道,奥尔良的方法调用什么时候返回。


PS: 我用await grain.foo() 尝试了上面的代码,它按顺序打印谷物输出。但是这种方法的问题是,await 仅在整个 foo() 完成时才返回,而我希望它在遇到 await 语句时返回。

【问题讨论】:

  • 您实际上想要完成什么?您是否希望颗粒并行运行但以确定的顺序返回?
  • From this document,看起来像谷子生成的任务在谷子上下文中执行。文档建议您可以使用Task.Run(() =&gt; {...}) 来代替使用默认任务计划程序。

标签: c# asp.net async-await orleans


【解决方案1】:

我会分两部分回答:

  1. 为什么不希望在某些远程调用的第一个 await 之前阻塞
  2. 所见即所得

从一开始:Orleans 是普通的 C#,但是关于 C# 在这种情况下如何工作的假设缺少一些细节(下文将解释)。 Orleans 专为可扩展的分布式系统而设计。有一个基本假设是,如果您在某个grain 上调用一个方法,那么该grain 当前可能在另一台机器上被激活。即使在同一台机器上,每个grain 与其他grain 异步运行,通常在单独的线程上。

为什么不希望在某个远程调用的第一个 await 之前阻塞

如果一台机器调用另一台机器,这需要一些时间(例如,由于网络)。 因此,如果您有一台机器上的线程调用另一台机器上的对象,并且您想阻塞该线程,直到该对象中出现await 语句,那么您将阻塞该线程很长一段时间。线程必须等待网络消息到达远程机器,以便在远程grain激活时调度它,让grain执行直到第一个await,然后远程机器发送消息通过网络回到第一台机器说“嘿,第一个等待被击中”。

这样的阻塞线程不是一种可扩展的方法,因为 CPU 要么在线程被阻塞时处于空闲状态,要么必须创建许多(昂贵的)线程以保持 CPU 忙于处理请求。每个线程在预先分配的堆栈空间和其他数据结构方面都有成本,线程之间的切换对 CPU 也是有成本的。

因此,希望现在可以清楚为什么在远程颗粒到达其第一个await 之前阻塞调用线程是不可取的。现在,让我们看看线程是如何在奥尔良没有被阻塞的。

所见即所得

请考虑您的grain 对象不是您编写的grain implementation 类的实例,而是一个“grain 引用”。

您可以使用以下代码创建 grain 对象:

var grain = grainFactory.GetGrain<IMyGrainInterface>("guest@myservice.com");

您从GetGrain 返回的对象是一个grain 引用。它实现了IMyGrainInterface,但它不是您编写的grain 类的一个实例。相反,它是 Orleans 为您生成的一个类。此类是您要调用的远程颗粒的表示,它是对它的引用。

所以当你写一些代码时:

grain.foo(i);

发生的情况是生成的类调用 Orleans 运行时向远程grain 激活发出foo 请求。

例如,生成的代码实际上可能如下所示:

public Task foo(int i)
{
    return base.InvokeMethodAsync(118718866, new object[]{ i });
}

这些细节对你来说是隐藏的,但你可以去你项目中的obj目录下找到它们。

所以你可以看到在生成的foo方法中实际上根本没有await!它只是要求 Orleans 运行时调用一个带有一些奇怪整数和一些对象数组的方法。

在远程端,类似的生成类执行相反的操作:它接受您的请求并将其转换为对您编写的实际grain 代码的直接方法调用。在远程系统中,线程将执行到您的grain代码中的第一个await,然后将执行返回给调度程序,就像在“普通C#”中一样。

另外:在 RPC 术语中,grain 引用大致相当于一个代理对象:即,它是一个表示远程对象的对象。为 WCF 或 gRPC 等传统 RPC 框架编写的相同代码的行为方式与 Orleans 相同:当客户端调用服务器上的方法时,您的线程将不会被阻塞,直到第一个 await

【讨论】:

  • 感谢@ReubenBond 的详细回答,解决了许多误解。我查看了 Orleans 代码中 InvokeMethodAsync() 的实现,似乎返回的任务是使用 TaskCompletionSource 形成的,而不是在远程机器上运行的实际任务(这很有意义,正如 Reuben 在第一个他回答的一部分)。
  • 谢谢,鲁本!但是,正如我在 this question 中解释的那样,这会阻止流水线操作。你对此有什么建议吗?
  • 是的,@ShengquanNi:您可以使用[AlwaysInterleave][Reentrant],正如我在回答here 中所解释的那样。
猜你喜欢
  • 1970-01-01
  • 2021-07-15
  • 1970-01-01
  • 2011-09-07
  • 1970-01-01
  • 2020-09-13
  • 2019-02-10
  • 2015-01-06
  • 2015-07-26
相关资源
最近更新 更多