Разница между вызовом метода в обычном C# и Орлеаном
Я использую Orleans в режиме LocalHost Clustering, и в настоящее время у меня есть 1 зерно и клиент.
// 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()
и печатает вывод зерна по порядку. Но проблема с этим подходом заключается в том, что ожидание возвращается только тогда, когда завершается весь foo(), тогда как я хочу, чтобы оно возвращалось, когда оно достигает оператора await.
1 ответ
Я отвечу в двух частях:
- Почему не желательно блокировать до первого
await
на какой-то удаленный звонок - Как то, что вы видите, это то, что вы должны ожидать
С самого начала: Орлеан - это обычный C#, но в предположениях о том, как C# работает в этом случае, отсутствуют некоторые детали (которые объяснены ниже). Orleans разработан для масштабируемых распределенных систем. Существует базовое предположение, что если вы вызываете метод для некоторого зерна, то это зерно в настоящее время может быть активировано на отдельном компьютере. Даже если он находится на одном и том же компьютере, каждое зерно асинхронно движется к другим зернам, часто в отдельном потоке.
Почему не желательно блокировать до первого await
на какой-то удаленный звонок
Если одна машина вызывает другую, это занимает некоторое время (например, из-за сети). Так что если у вас есть поток на одном компьютере, вызывающий объект на другом, и вы хотите заблокировать этот поток до await
оператор в этом объекте, то вы блокируете этот поток на значительное количество времени. Поток должен будет ждать поступления сетевого сообщения на удаленную машину, чтобы он был запланирован при удаленной активации зерна, чтобы зерно выполнялось до первого await
и затем для удаленной машины отправить сообщение по сети на первую машину, чтобы сказать "эй, первое ожидание было достигнуто".
Подобная блокировка потоков не является масштабируемым подходом, поскольку ЦП либо простаивает, когда поток заблокирован, либо необходимо создать много (дорогих) потоков, чтобы ЦП занимал обработку запросов. Каждый поток имеет стоимость в виде предварительно выделенного стекового пространства и других структур данных, а переключение между потоками имеет стоимость для ЦП.
Итак, надеюсь, теперь понятно, почему нежелательно блокировать вызывающий поток, пока удаленное зерно не достигнет своего первого await
, Теперь давайте посмотрим, как получается, что поток не блокируется в Орлеане.
Как то, что вы видите, это то, что вы должны ожидать
Считайте, что ваш grain
Объект не является экземпляром класса реализации зерна, который вы пишете, но вместо этого он является "ссылкой на зерно".
Вы создаете это grain
объект, используя что-то вроде следующего кода:
var grain = grainFactory.GetGrain<IMyGrainInterface>("guest@myservice.com");
Объект, который вы получите от GetGrain
это ссылка на зерно. Это реализует IMyGrainInterface
, но это не экземпляр класса зерна, который вы написали. Вместо этого это класс, который Орлеан генерирует для вас. Этот класс является представлением удаленного зерна, которое вы хотите вызвать, это ссылка на него.
Поэтому, когда вы пишете некоторый код вроде:
grain.foo(i);
что происходит, это сгенерированные вызовы класса в среду выполнения Орлеана, чтобы сделать foo
запрос на удаленную активацию зерна.
В качестве примера, вот как на самом деле может выглядеть сгенерированный код:
public Task foo(int i)
{
return base.InvokeMethodAsync(118718866, new object[]{ i });
}
Эти детали скрыты от вас, но вы можете пойти и найти их, если вы посмотрите под obj
каталог в вашем проекте.
Таким образом, вы можете увидеть, что на самом деле нет await
в сгенерированном foo
метод вообще! Он просто просит среду выполнения Орлеана вызвать метод с каким-то странным целым числом и некоторым массивом объектов.
На удаленном конце сгенерированный класс аналогичного типа делает обратное: он принимает ваш запрос и превращает его в прямой вызов метода для фактического кода, который вы написали. В удаленной системе поток будет выполняться до первого await
в вашем коде зерна, а затем вернуть выполнение обратно в планировщик, как в "обычном C#".
Кроме того: в терминах RPC ссылка на зерно примерно эквивалентна прокси-объекту, т. Е. Это объект, представляющий удаленный объект. Тот же код, написанный для традиционной среды RPC, такой как WCF или gRPC, будет вести себя так же, как Орлеан: ваш поток не будет заблокирован до первого await
когда клиент вызывает метод на сервере.