普通C#和Orleans中的方法调用之间的区别 [英] Difference between method call in normal C# and Orleans

查看:237
本文介绍了普通C#和Orleans中的方法调用之间的区别的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在localHost Clustering模式下运行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
.
.

普通C#中,异步函数仅在命中await时返回.在这种情况下,谷物产量应该是连续的.正如我们在上面看到的那样,谷物产量是乱序的.因此,在命中await语句之前任务将返回. 我的问题是,奥尔良中的方法调用与普通C#之间有什么区别.

我看到了这篇文章,其中要求类似的问题和答复表明,方法调用的两种情况是不同的,因为我们在奥尔良调用接口. 我想知道,该方法调用何时在奥尔良返回.


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

解决方案

我将分两部分进行回答:

  1. 为什么不希望在某个远程呼叫的第一个await前阻塞
  2. 您所看到的是您应有的期望

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

为什么不希望在某些远程呼叫的第一个await前阻塞

如果一台计算机呼叫另一台计算机,则需要花费一些时间(例如,由于网络原因). 因此,如果您在一台计算机上有一个线程在调用另一台计算机上的对象,并且想要将该线程阻塞直到该对象内的await语句,那么您将在相当长的时间内阻塞该线程.线程将必须等待网络消息到达远程计算机,以便在远程启动谷物时对其进行调度,直到第一个await执行谷物,然后远程计算机才能发送消息.通过网络回到第一台机器,说嘿,第一次等待被击中".

像这样的阻塞线程不是可扩展的方法,因为CPU要么在线程阻塞时处于空闲状态,要么必须创建许多(昂贵的)线程才能使CPU忙于处理请求.就预分配的堆栈空间和其他数据结构而言,每个线程都要付出代价,而在线程之间进行切换则要为CPU付出代价.

因此,希望现在很清楚为什么在远程粒度达到其第一个await之前不希望阻塞调用线程.现在,让我们看看如何在奥尔良中不阻塞线程.

您所看到的是您应有的期望

请考虑您的grain对象不是您编写的grain 实现类的实例,而是它是晶粒引用".

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

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

GetGrain返回的对象是 grain引用.它实现了IMyGrainInterface,但它不是您编写的Grain类的实例.相反,它是奥尔良为您生成的类.此类是您要调用的远程粒度的表示形式,是对它的引用.

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

grain.foo(i);

发生的事情是生成的类调用Orleans运行时以对远程谷物激活发出foo请求.

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

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

这些详细信息对您隐藏了,但是如果您查看项目中的obj目录下的内容,则可以查找它们.

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

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

此外:在RPC术语中,grain引用大致等效于代理对象:即,它是代表的对象.为传统的RPC框架(如WCF或gRPC)编写的代码与Orleans的行为相同:在客户端调用服务器上的方法之前,直到第一个await,您的线程都不会被阻塞.

I am running Orleans in localHost Clustering mode and currently have 1 grain and a client.

// 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);
}

The output of this was as below:

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
.
.

In normal C#, the async function returns only when it hits await. In that case, the grain output should have been consecutive. As we can see above, the grain outputs are out of order. So, the Task is returning before hitting the await statement. My question is what is the difference between method call in Orleans and normal C#.

I saw this post which asks a similar question and the replies suggest that the two cases of method calls are different because we call an interface in Orleans. I would like to know, when does the method call return in Orleans.


PS: I tried the above code with await grain.foo() and it prints the grain output in order. But the problem with that approach is, await returns only when the entire foo() completes, whereas I want it to return when it hits await statement.

解决方案

I'll answer in two parts:

  1. Why it is not desirable to block until the first await on some remote call
  2. How what you are seeing is what you should expect

From the outset: Orleans is normal C#, but the assumptions about how C# works in this case are missing some details (which are explained below). Orleans is designed for scalable, distributed systems. There is a basic assumption that if you call a method on some grain, that grain might be currently activated on a separate machine. Even if it is on the same machine, each grain runs asynchronously to other grains, often on a separate thread.

Why it is not desirable to block until the first await on some remote call

If one machine calls another machine, that takes some time (eg, because of the network). So if you have a thread on one machine calling into an object on another and you want to block that thread until an await statement within that object, then you're blocking that thread for a significant amount of time. The thread would have to wait for the network message to arrive on the remote machine, for it to be scheduled on the remote grain activation, for the grain to execute until the first await, and then for the remote machine to send a message back over the network to the first machine to say "hey, the first await was hit".

Blocking threads like that is not a scalable approach because the CPU is either idle while the thread is blocked or many (expensive) threads must be created in order to keep the CPU busy processing requests. Each thread has a cost in terms of pre-allocated stack space and other data structures, and switching between threads has a cost for the CPU.

So, hopefully it is clear now why it would not be desirable to block the calling thread until the remote grain hits its first await. Now, let's see how come the thread is not being blocked in Orleans.

How what you are seeing is what you should expect

Consider that your grain object is not an instance of the grain implementation class that you write, but is instead it is a 'grain reference'.

You create that grain object by using something like the following code:

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

The object you get back from the GetGrain is a grain reference. It implements IMyGrainInterface, but it is not an instance of the grain class that you wrote. Instead, it is a class which Orleans generates for you. This class is a representation of the remote grain which you want to call, it's a reference to it.

So when you write some code like:

grain.foo(i);

what happens is the generated class calls into the Orleans runtime to make the foo request to the remote grain activation.

As an example, here's what the generated code might actually look like:

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

Those details are hidden from you, but you can go and find them if you look under the obj directory in your project.

So you can see that there is actually no await in the generated foo method at all! It simply asks the Orleans runtime to invoke a method with some weird integer and some object array.

On the remote end, a similar kind of generated class does the reverse: it takes your request and turns it into a direct method call on the actual grain code that you wrote. In the remote system, the thread will execute up to the first await in your grain code and then yield execution back to the scheduler, just like in "normal C#".

Aside: in RPC terms, a grain reference is roughly equivalent to a proxy object: i.e, it's an object which represents the remote object. The same code written for a traditional RPC framework like WCF or gRPC would behave in the same way as Orleans: your thread will not be blocked until the first await when a client calls a method on a server.

这篇关于普通C#和Orleans中的方法调用之间的区别的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆