Где хранятся параметры типа значения ref для асинхронных вызовов методов в Microsoft CLR? - PullRequest
5 голосов
/ 13 октября 2010

Я понимаю, что это деталь реализации.Мне действительно любопытно, что это за деталь реализации в в Microsoft CLR.

Теперь, потерпите меня, потому что я не изучал CS в колледже, так что я мог упустить некоторые фундаментальные принципы.

Но мое понимание «стека» и «кучи» в том виде, в каком они реализованы в CLR в его нынешнем виде, я думаю, является твердым.Я не собираюсь делать некоторые неточные зонтичные утверждения, такие как, например, «типы значений хранятся в стеке».Но в большинстве распространенных сценариев - простые локальные переменные типа vanilla, типа значения, которые либо передаются в качестве параметров, либо объявляются в методе и не содержатся внутри переменных типа «замыкание» - хранятся в стеке (сновав CLR от Microsoft).

Наверное, я не уверен, откуда приходят ref параметры типа значения.

Первоначально я думал, что, если стек вызовов выглядит следующим образом (left = bottom):

A() -> B() -> C()

... тогда локальная переменная объявляется в области действия A и передается как параметр ref B все еще может храниться в стеке - не так ли? B просто понадобится место в памяти, где хранится эта локальная переменная в кадре A (простите, если это неправильная терминология; я думаю, что понятно, что я имею в виду,в любом случае).

Я понял, что это не может быть строго правдой, хотя, когда мне пришло в голову, что я мог бы сделать это:

delegate void RefAction<T>(ref T arg);

void A()
{
    int x = 100;

    RefAction<int> b = B;

    // This is a non-blocking call; A will return immediately
    // after this.
    b.BeginInvoke(ref x, C, null);
}

void B(ref int arg)
{
    // Putting a sleep here to ensure that A has exited by the time
    // the next line gets executed.
    Thread.Sleep(1000);

    // Where is arg stored right now? The "x" variable
    // from the "A" method should be out of scope... but its value
    // must somehow be known here for this code to make any sense.
    arg += 1;
}

void C(IAsyncResult result)
{
    var asyncResult = (AsyncResult)result;
    var action = (RefAction<int>)asyncResult.AsyncDelegate;

    int output = 0;

    // This variable originally came from A... but then
    // A returned, it got updated by B, and now it's still here.
    action.EndInvoke(ref output, result);

    // ...and this prints "101" as expected (?).
    Console.WriteLine(output);
}

Итак, в приведенном выше примере, где находится x (в области действия A ) сохранено?И как это работает?Это в штучной упаковке?Если нет, подлежит ли он сборке мусора сейчас, несмотря на то, что он является типом значения?Или память может быть немедленно восстановлена?

Я прошу прощения за многословный вопрос.Но даже если ответ довольно прост, возможно, он будет информативным для тех, кому интересно в будущем то же самое.

Ответы [ 3 ]

4 голосов
/ 13 октября 2010

Я не верю, что когда вы используете BeginInvoke() и EndInvoke() с ref или out аргументами, вы действительно передаёте переменные по ссылке. факт, что мы должны вызывать EndInvoke() с параметром ref, должен быть ключом к этому.

Давайте изменим ваш пример, чтобы продемонстрировать поведение, которое я описываю:

void A()
{
    int x = 100;
    int z = 400;

    RefAction<int> b = B;

    //b.BeginInvoke(ref x, C, null);
    var ar = b.BeginInvoke(ref x, null, null);
    b.EndInvoke(ref z, ar);

    Console.WriteLine(x);  // outputs '100'
    Console.WriteLine(z);  // outputs '101'
}

Если вы посмотрите на вывод сейчас, вы увидите, что значение x фактически не изменилось. Но z теперь содержит значение обновления.

Я подозреваю, что компилятор изменяет семантику передачи переменных на ref, когда вы используете асинхронные методы Begin / EndInvoke.

После просмотра IL, созданного этим кодом, выясняется, что ref аргументы BeginInvoke() все еще передаются by ref. Хотя Reflector не показывает IL для этого метода, я подозреваю, что он просто не передает параметр в качестве аргумента ref, а вместо этого создает отдельную переменную за сценой для передачи в B(). Когда вы затем вызываете EndInvoke(), вы должны снова ввести аргумент ref, чтобы получить значение из асинхронного состояния. Вполне вероятно, что такие аргументы на самом деле хранятся как часть (или в сочетании с) объекта IAsyncResult, который необходим для окончательного получения их значений.

Давайте подумаем, почему поведение, вероятно, работает таким образом. Когда вы делаете асинхронный вызов метода, вы делаете это в отдельном потоке. Этот поток имеет свой собственный стек и поэтому не может использовать типичный механизм псевдонимов переменных ref/out. Однако, чтобы получить какие-либо возвращаемые значения из асинхронного метода, вам необходимо в конечном итоге вызвать EndInvoke(), чтобы завершить операцию и получить эти значения. Однако вызов EndInvoke() может происходить так же легко в совершенно другом потоке, что и исходный вызов BeginInvoke() или фактическое тело метода. Ясно, что стек вызовов не является подходящим местом для хранения таких данных - тем более что поток, используемый для асинхронного вызова, может быть переопределен для другого метода после завершения асинхронной операции. В результате для «маршалирования» возвращаемого значения и аргументов out / ref из метода, вызываемого обратно на сайт, где они в конечном итоге будут использоваться, необходим какой-то другой механизм, кроме стека.

Я считаю, что этот механизм (в реализации Microsoft .NET) является объектом IAsyncResult. Фактически, если вы изучите объект IAsyncResult в отладчике, вы заметите, что в непубличных членах существует _replyMsg, который содержит коллекцию Properties. Эта коллекция содержит такие элементы, как __OutArgs и __Return, данные которых отражают их тезки.

РЕДАКТИРОВАТЬ: Вот теория о дизайне асинхронного делегата, который мне приходит в голову. Кажется вероятным, что подписи BeginInvoke() и EndInvoke() были выбраны, чтобы быть как можно ближе друг к другу, чтобы избежать путаницы и улучшить ясность. Метод BeginInvoke() на самом деле не требует для принятия ref/out аргументов - поскольку ему нужно только их значение ... а не их идентификация (поскольку он никогда не будет присваивать им что-либо обратно). Однако было бы очень странно (например) иметь BeginInvoke() вызов, который принимает int и EndInvoke() вызов, который принимает ref int. Теперь, возможно, есть технические причины, по которым начальные / конечные вызовы должны иметь одинаковые подписи, но я думаю, что преимущества ясности и симметрии достаточны для проверки такой конструкции.

Все это, конечно, деталь реализации компилятора CLR и C # и может измениться в будущем. Интересно, однако, что существует вероятность путаницы - если вы ожидаете, что исходная переменная, переданная в BeginInvoke(), будет фактически изменена. Это также подчеркивает важность вызова EndInvoke() для завершения асинхронной операции.

Возможно, кто-то из команды C # (если они увидят этот вопрос) мог бы предложить более глубокое понимание деталей и вариантов дизайна, стоящих за этой функциональностью.

3 голосов
/ 13 октября 2010

CLR полностью вне цикла, JIT-компилятор должен сгенерировать соответствующий машинный код, чтобы получить аргумент, переданный по ссылке. Что само по себе является деталью реализации, для разных архитектур машин существуют разные дрожания.

Но обычные делают это точно так, как это делает программист C, они передают указатель на переменную. Этот указатель передается в регистре ЦП или в кадре стека, в зависимости от того, сколько аргументов принимает метод.

Там, где переменная живет, значения не имеет, указатель на переменную в кадре стека вызывающей стороны так же действителен, как указатель на член объекта ссылочного типа, который хранится в куче. Сборщик мусора знает разницу между ними благодаря значению указателя, регулирующему указатель при необходимости при перемещении объекта.

Ваш фрагмент кода вызывает магию внутри .NET Framework, которая необходима для выполнения маршалинга вызовов из одного потока в другой. Это тот же вид сантехники, который делает Remoting работ. Чтобы сделать такой вызов, необходимо создать новый кадр стека в потоке, в котором выполняется вызов. Код удаленного взаимодействия использует определение типа делегата, чтобы знать, как должен выглядеть этот кадр стека. И он может иметь дело с аргументами, передаваемыми по ссылке, он знает, что ему нужно выделить слот в кадре стека для хранения указанной ссылки, i в вашем случае. Вызов BeginInvoke инициализирует копию переменной i в удаленном фрейме стека.

То же самое происходит при вызове EndInvoke (), результаты копируются обратно из стекового фрейма в поток пула потоков. Ключевым моментом является то, что на самом деле нет указателя на переменную i , есть указатель на его копию.

Не совсем уверен, что этот ответ очень ясен, имея некоторое представление о том, как работают процессоры, и немного знания C, поэтому концепция указателя является кристальной, может очень помочь.

2 голосов
/ 13 октября 2010

Посмотрите на код, сгенерированный с помощью рефлектора, чтобы узнать. Я предполагаю, что создается анонимный класс, содержащий x, например, когда вы используете замыкания (лямбда-выражения, которые ссылаются на переменные в текущем кадре стека) Забудьте об этом и прочитайте другие ответы.

...