Асинхронный вызов метода - Особенности 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 и затем захватил бы другой поток для вызова обратного вызова, что было бы расточительством! При истощении пула потоков пришлось бы ждать свободного потока для вызова обратного вызова, что было бы кошмаром.