Асинхронный вызов метода - Особенности IAsyncResult

ОГЛАВЛЕНИЕ

Особенности IAsyncResult

Как EndInvoke отдает выходные параметры и обновленные параметры ref? Как EndInvoke выбрасывает исключение, выброшенное в функции? Допустим, был вызван BegineInvoke для Foo, а затем Foo закончила выполняться, и теперь обычно вызвался бы EndInvoke, но что если вызвать EndInvoke через 20 минут после завершения Foo? EndInvoke по-прежнему отдаст выходные параметры или параметры ref и выбросит то исключение (если оно было выброшено). Где хранится вся эта информация? Как EndInvoke получает все эти данные намного позже завершения функции? С помощью объекта IAsyncResult. Этот объект хранит всю информацию о вызове функции. EndInvoke принимает один параметр – объект типа IAsyncResult. Этот объект содержит следующую информацию:
•    Завершена ли функция?
•    Ссылка на делегат, используемый для BeginInvoke
•    Все выходные параметры и их значения
•    Все параметры ref и их измененные значения
•    Возвращаемое значение
•    Исключение, если оно было выброшено
•    И не только…

IAsyncResult может казаться безобидным, потому что является всего лишь интерфейсом для нескольких свойств, но на самом деле это объект типа System.Runtime.Remoting.Messaging.AsyncResult.

AsyncResult содержит объект по имени _replyMsg типа System.Runtime.Remoting.Messaging.ReturnMessage.

Пришлось уменьшить изображение выше, чтобы не прокручивать вправо для его чтения. Щелкните по изображению, чтобы просмотреть его
Четко видно возвращаемое значение, выходной параметр и параметры ref. Есть даже свойство «исключение» для хранения исключения. В окне отладки был развернут OutArgs, чтобы показать значение 200 и ссылку на вновь выделенный ArrayList. В свойстве ReturnValue видна строка “Спасибо за прочтение статьи”. Если бы имелось исключение, то EndInvoke выбросил бы его для перехвата. Это доказывает, что вся информация о вызове функции хранится в объекте IAsyncResult, возвращаемом из BeginInvoke, Он похож на ключ к данным. Если потерять этот объект, то вообще не удастся узнать выходные параметры, параметры ref и возвращаемое значение. Без этого объекта невозможно перехватить исключение. Это ключ, при потере которого информация навсегда теряется в дебрях среды выполнения .NET.

Использование делегата обратного вызова в стиле "не вызывайте меня, я вызову вас!"

Было объяснено, как передаются параметры, как передается состояние, и что метод выполняется в потоке в ThreadPool. Не было рассмотрено лишь уведомление о завершении выполнения метода. Блокирование и ожидание завершения метода не достигает многого. Чтобы получить уведомление, когда метод завершится, надо задать делегат обратного вызова в BeginInvoke. Посмотрите на следующие две функции:

private void CallFooWithOutAndRefParametersWithCallback()
{
    // создать параметры для передачи функции
    string strParam1 = "Param1";
    int intValue = 100;
    ArrayList list = new ArrayList();
    list.Add("Item1");

    // создать делегат
    DelegateWithOutAndRefParameters delFoo =
        new DelegateWithOutAndRefParameters(FooWithOutAndRefParameters);

    delFoo.BeginInvoke(strParam1,
        out intValue,
        ref list,
        new AsyncCallback(CallBack), // делегат обратного вызова
        null);
}

private void CallBack(IAsyncResult ar)
{
    // определить выходной параметр
    int intOutputValue;
    ArrayList list = null;

    // первый случай - IAsyncResult в объект AsyncResult, чтобы получить
    // делегат, использованный для вызова функции.
    AsyncResult result = (AsyncResult)ar;

    // захватить делегат
    DelegateWithOutAndRefParameters del =
        (DelegateWithOutAndRefParameters) result.AsyncDelegate;

    // теперь, когда есть делегат,
    // для него вызывается EndInvoke, чтобы получить всю
    // информацию о вызове метода.

    string strReturnValue = del.EndInvoke(out intOutputValue,
        ref list, ar);
}

Делегат был передан функции CallBack при вызове BeginInvoke. .NET вызовет, когда метод FooWithOutAndRefParameters закончит выполняться. Как и прежде, надо вызвать EndInvoke, чтобы получить выходные параметры. Чтобы вызвать EndInvoke, пришлось написать лишний код для получения делегата.

AsyncResult result = (AsyncResult)ar;
// захватить делегат
DelegateWithOutAndRefParameters del =
    (DelegateWithOutAndRefParameters) result.AsyncDelegate;

В каком потоке выполняется обратный вызов?

.NET вызывает обратный вызов с помощью делегата, то есть .NET вызывает этот делегат. Надо знать, в каком потоке выполняется код. Для получения четкого представления о происходящем в который раз была изменена функция Foo так, чтобы включила в себя информацию о потоке и добавила задержку в 4 секунды.

private string FooWithOutAndRefParameters(string param1,
        out int param2, ref ArrayList list)
{
    // вывести на экран информацию о потоке
    Trace.WriteLine("In FooWithOutAndRefParameters: Thread Pool? "
        + Thread.CurrentThread.IsThreadPoolThread.ToString() +
        " Thread Id: " + Thread.CurrentThread.GetHashCode());

    // ждать 4 секунды, словно эта функция долго выполняется.
    Thread.Sleep(4000);

    // изменить данные
    param1 = "Modify Value for param1";
    param2 = 200;
    list = new ArrayList();

    return "Thank you for reading this article";
}

Также была добавлена информация о потоке в функцию обратного вызова:

private void CallBack(IAsyncResult ar)
{
    // в каком потоке находится?
    Trace.WriteLine("In Callback: Thread Pool? "
        + Thread.CurrentThread.IsThreadPoolThread.ToString() +
        " Thread Id: " + Thread.CurrentThread.GetHashCode());

    // определить выходной параметр
    int intOutputValue;
    ArrayList list = null;

    // первый случай - IAsyncResult в объект AsyncResult,
    // чтобы получить делегат, использованный для вызова функции.
    AsyncResult result = (AsyncResult)ar;

    // захватить делегат
    DelegateWithOutAndRefParameters del =
        (DelegateWithOutAndRefParameters) result.AsyncDelegate;

    // теперь, когда есть делегат, для него вызывается EndInvoke, чтобы
    // получить всю информацию о вызове метода.
    string strReturnValue = del.EndInvoke(out intOutputValue, ref list, ar);
}
FooWithOutAndRefParameters выполняется многократно с помощью кнопки на форме.
private void button4_Click(object sender, EventArgs e)
{
    CallFooWithOutAndRefParametersWithCallback();
}

Вывод после трехкратного нажатия кнопки (трехкратного вызова функции) следующий:

In FooWithOutAndRefParameters: Thread Pool? True Thread Id: 7
In FooWithOutAndRefParameters: Thread Pool? True Thread Id: 12
In FooWithOutAndRefParameters: Thread Pool? True Thread Id: 13
In Callback: Thread Pool? True Thread Id: 7
In Callback: Thread Pool? True Thread Id: 12
In Callback: Thread Pool? True Thread Id: 13

Функция Foo выполняется трижды, одна за другой, в трех разных потоках. Все потоки находятся в пуле потоков. Обратный вызов тоже выполняется трижды, соответственно, и все они тоже находятся в пуле потоков. Оказывается, что обратный вызов выполняется в том же идентификаторе потока, что и Foo. Поток 7 выполняет Foo; 4 секунды спустя обратный вызов тоже выполняется в потоке 7. То же самое свойственно потокам 12 и 13. Словно обратный вызов является продолжением функции Foo. Кнопку нажимали много раз, чтобы узнать, вызовется ли обратный вызов когда-нибудь в идентификаторе потока, отличном от того, в котором выполняется Foo. Этого не удалось добиться, что имеет смысл. Представьте, .NET захватил бы поток для вызова Foo и затем захватил бы другой поток для вызова обратного вызова, что было бы расточительством! При истощении пула потоков пришлось бы ждать свободного потока для вызова обратного вызова, что было бы кошмаром.