普通 C# 和 Orleans 中方法调用的区别

Difference between method call in normal C# and Orleans

我是 运行 本地主机集群模式下的 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# 中,异步函数 return 仅在它命中 await 时才运行。那样的话,粮食产量应该是连续的。正如我们在上面看到的,谷物输出是无序的。因此,任务在点击 await 语句之前 returning。 我的问题是 Orleans 和普通 C# 中的方法调用有什么区别。

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


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

我分两部分回答:

  1. 为什么在某些远程调用 await 之前阻塞是不可取的
  2. 所见即所得

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

为什么在某些远程调用 await 之前阻塞是不可取的

如果一台机器调用另一台机器,那需要一些时间(例如,由于网络)。 因此,如果您在一台机器上有一个线程调用另一台机器上的一个对象,并且您想阻塞该线程直到该对象中的 await 语句,那么您将阻塞该线程很长一段时间。线程必须等待网络消息到达远程机器,以便在远程 grain 激活时安排它,让 grain 执行到第一个 await,然后让远程机器发送通过网络返回给第一台机器的消息 "hey, the first await was hit".

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

的成本

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

您所看到的是您所期望的

请考虑您的 grain 对象不是您编写的 grain 实现 class 的一个实例,而是一个 'grain reference'.

您可以使用类似于以下代码的内容创建 grain 对象:

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

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

因此,当您编写如下代码时:

grain.foo(i);

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

例如,生成的代码实际上可能是这样的:

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

这些细节对你是隐藏的,但如果你在项目的 obj 目录下查看,你可以找到它们。

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

在远程端,生成的类似 class 做相反的事情:它接收您的请求并将其转换为对您编写的实际 grain 代码的直接方法调用。在远程系统中,线程将执行到您的 grain 代码中的第一个 await,然后将执行返回给调度程序,就像在 "normal C#".

中一样

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