Страницы

Ярлыки

ДШИ-200 (1) КСВУ-6 (1) ЛЧМ (1) МДР-23 (1) микроконтроллер (1) перенаправление (1) С (1) структуры (1) учебный курс (1) AC/DC (1) ADC (1) ADS1248 (1) Altium (1) Altuim (1) Amolifer (1) ARM (1) assembler (2) Asynchronous (1) at command (3) at#eaddr (1) at#epassw (1) at#esmtp (1) at#euser (1) at#gprs (1) at#selint=2 (1) at#sgact (1) at#tcpatcmdseq (1) ATX (1) AVR (2) bit (1) boost (1) boot (2) bootlloader (1) C (6) C# (7) C++ (1) CMSIS (1) command (1) CP2101 (1) CSD (1) Danfoss (6) DBGMCU (1) debug (1) debug.ini (1) delegate (1) Discovery (1) DMA (1) DRV8805 (1) DWT (1) e-mail (1) email (1) Exel (1) exFAT (1) FC-051 (1) gl868-dual (2) gl868-dual cmux (1) GPIO (2) GSM (1) I2C (1) IAR (1) ID (1) Invoke (1) Keil (3) LabVIEW (1) Linux (1) LMP7721 (1) LoRa (3) mdmread (1) memory (1) MODBUS (1) Operation Amplifer (1) pack (1) printf (2) printf() (1) RCC (1) retargetting (1) RFM95/96/87/98(W) (1) RS232 (4) RS485 (1) RSAPI.DLL (1) RSS (1) RTC (2) send (2) SerialPort (1) Silabs (1) spl (1) standard peripherals library (1) startup (1) stepper (2) STlink (1) STlink/V2 (2) STM32 (10) stm32 stm32f10x (1) STM32DBG.IN (1) STM32F (19) STM32F103 (4) struct (1) Structure (1) system (1) SystemInit (1) Task (1) telit (5) thread (4) TIM (1) Type Cast (1) UART (1) uni-trend (1) USART (6) USB (1) UT61B (1) viewer (1)

пятница, 5 февраля 2016 г.

Asynchronous Method Invocation (Асинхронный вызов метода)

Асинхронные вызовы методов

Введение

В этой статье мы поговорим об асинхронном вызове методов и о том, как это делается. После того, как я наигрался с делегатами, потоками и асинхронными вызовами, было бы грехом не поделиться моими знаниями, я надеюсь что это поможет вам и вы не будете в час ночи лезть в MSDN и проклинать себя за то, что связались с этими компьютерами. Мы пойдем маленькими шажками, с большим количеством примеров. В целом я расскажу о том, как асинхронно вызывать методы, как передавать в них параметры и как узнать о их завершении. В конце мы рассмотрим паттерн Команда (Command Pattern) в немного упрощенном представлении. Самое большое преимущество асинхронных методов в .NET – это то, что вы можете взять любой метод вашего проекта и просто вызвать его асинхронно, без какого либо вмешательства в его код. Хотя все происходит за кулисами .NET, очень важно понимать, что же там происходит и мы это выясним.

Синхронно и асинхронно

Давайте я попытаюсь объяснить синхронный и асинхронный вызов метода на примере.

Синхронный вызов метода

Предположим, у нас есть функция Foo(), которая требует 10 секунд на выполнение.

private void Foo()
{
// sleep for 10 seconds.
Thread.Sleep(10000);
}

Итак, когда ваше приложение вызывает метод Foo(), ему требуется 10 секунд на то, что бы Foo() завершился и управление перешло вызвавшему его потоку. Если же мы хотим вызвать этот метод 100 раз, то, как вы уже догадались, вам потребуется 1000 секунд для возвращения к вызвавшему потоку. Этот тип вызова и называется синхронным.
  1. Вызов функции Foo()
  2. Foo() закончила выполнение
  3. Управление переходит обратно к вызвавшему потоку
Давайте теперь вызовем Foo() используя делегат, потому что почти все, что мы тут будем делать, мы будем делать с их помощь. К счастью в .NET уже есть делегат, который вызывает функцию, которая не принимает параметров и не возвращает значения. Он называется MethodeInvoker. Давайте с ним немного поиграемся.
// Создание делегата MethodInvoker
// указывающий на метод Foo()
MethodInvoker simpleDelegate = new MethodInvoker(Foo);

// Вызов Foo
simpleDelegate.Invoke();
Даже в данном примере мы вызываем Foo() синхронно. Вызывающий поток должен дождаться, когда фукнция Invoke() закончит выполнение и уже после этого управление переходит вызывающему потоку.

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

А что, если мы хотим вызывать Foo() и не ждать его выполнения? На самом деле, что если я не хочу заботиться о том, когда они завершатся? Скажем, мне нужно выполнить Foo() 100 раз без ожидания завершения каждой функции. Этот принцип называется “Выстрелил и забыл” (Fire and Forget). Вы вызываете функцию, но не ждете ее, а просто забываете о ней. Но…. не забываете. Я не изменю ни строчки кода в моей фантастической функции Foo().
// Создание делегата MethodInvoker
// указывающий на метод Foo()
MethodInvoker simpleDelegate = new MethodInvoker(Foo);
// Вызываем функцию Foo() асинхронно
for(int i=0; i<100; i++)
simpleDelegate.BeginInvoke(null, null);
Немного прокомментирую этот код:
  • Обратите внимание, что BeginInvoke() выполняет функцию Foo(). Однако управление вызывающей стороне передается сразу, без ожидания завершения Foo().
  • Этот код не знает, когда завершится Foo(), об этом я расскажу позже.
  • BeginInvoke() используется вместо Invoke(). Пока не беспокойтесь о том, какие параметры принимает эта функция. Я расскажу о этом попозже.

Что творится за кулисами .NET

Для того, что бы фреймворку выполнить функцию асинхронно, требуется поток. Это не может быть текущим потоком, потому что тогда это уже будет синхронным вызовом и поток будет блокирован. Вместо этого, среда выполнения запрашивает выполнение в отдельном поток у .NET Thread Pool. Вы не должны описывать что-то еще для этого, все это делается в фоне. Но то, что это все делается прозрачно для вас, еще не значит, что вы не должны знать, как оно работает. Запомните несколько вещей:
  • Foo() выполняется в отдельном потоке,  который принадлежит .NET Thread Pool.
  • Обычно в .NET Thread Pool выполняется 25 потоков (вы можете изменить это) и каждый раз Foo() будет выполняться в одном из этих потоков. Каким именно, контролировать вы не можете..
  • Thread Pool имеет пределы. Когда все потоки уже использованы, асинхронное выполнение добавляет новые потоки в очередь. Это называется Thread Pool Starvation и является неким компромиссом в производительности.

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

Давайте рассмотрим пример, в котором потоковый пул (Thread Pool) будет голодать. Изменим нашу функцию так, что бы она ждала 30 секунд и выводила некоторую информацю:
  • Число доступных потоков в пуле
  • Проверку, что поток в пуле
  • ID потока
Теперь мы уже знаем, что пул содержит 25 потоков, поэтому я вызову нашу функцию 30 раз (посмотрим, что случится после 25-го вызова).
private void CallFoo30AsyncTimes()
{
// Создание делегата MethodInvoker
// указывающий на метод Foo()
MethodInvoker simpleDelegate = new MethodInvoker(Foo);

// Вызываем функцию Foo() асинхронно 30 раз
for (int i = 0; i < 30; i++)
{
// call Foo()
simpleDelegate.BeginInvoke(null, null);
}
}

private void Foo()
{
int intAvailableThreads, intAvailableIoAsynThreds;

//запрашиваем, сколько свободных потоков у нас есть,
//нас интересует только первый параметр
ThreadPool.GetAvailableThreads(out intAvailableThreads,
out intAvailableIoAsynThreds);

// форматируем сообщение для отчета
string strMessage =
String.Format(@"Is Thread Pool: {1},
Thread Id: {2} Free Threads {3}",
Thread.CurrentThread.IsThreadPoolThread.ToString(),
Thread.CurrentThread.GetHashCode(),
intAvailableThreads);

// проверяем, что поток находится в пуле
Trace.WriteLine(strMessage);

// создаем искусственную задержку
Thread.Sleep(30000);

return;
}
Вот что мне было выведено:
Is Thread Pool: True, Thread Id: 7 Free Threads 24
Is Thread Pool: True, Thread Id: 12 Free Threads 23
Is Thread Pool: True, Thread Id: 13 Free Threads 22
Is Thread Pool: True, Thread Id: 14 Free Threads 21
Is Thread Pool: True, Thread Id: 15 Free Threads 20
Is Thread Pool: True, Thread Id: 16 Free Threads 19
Is Thread Pool: True, Thread Id: 17 Free Threads 18
Is Thread Pool: True, Thread Id: 18 Free Threads 17
Is Thread Pool: True, Thread Id: 19 Free Threads 16
Is Thread Pool: True, Thread Id: 20 Free Threads 15
Is Thread Pool: True, Thread Id: 21 Free Threads 14
Is Thread Pool: True, Thread Id: 22 Free Threads 13
Is Thread Pool: True, Thread Id: 23 Free Threads 12
Is Thread Pool: True, Thread Id: 24 Free Threads 11
Is Thread Pool: True, Thread Id: 25 Free Threads 10
Is Thread Pool: True, Thread Id: 26 Free Threads 9
Is Thread Pool: True, Thread Id: 27 Free Threads 8
Is Thread Pool: True, Thread Id: 28 Free Threads 7
Is Thread Pool: True, Thread Id: 29 Free Threads 6
Is Thread Pool: True, Thread Id: 30 Free Threads 5
Is Thread Pool: True, Thread Id: 31 Free Threads 4
Is Thread Pool: True, Thread Id: 32 Free Threads 3
Is Thread Pool: True, Thread Id: 33 Free Threads 2
Is Thread Pool: True, Thread Id: 34 Free Threads 1
Is Thread Pool: True, Thread Id: 35 Free Threads 0
Is Thread Pool: True, Thread Id: 7 Free Threads 0
Is Thread Pool: True, Thread Id: 12 Free Threads 0
Is Thread Pool: True, Thread Id: 13 Free Threads 0
Is Thread Pool: True, Thread Id: 14 Free Threads 0
Is Thread Pool: True, Thread Id: 15 Free Threads 0
Давайте кое что уясним отсюда:
  • Обратите внимание, в первую очередь, на то, что все потоки выполнялись в пуле
  • Обратите внимание на то, что каждый раз, когда вызывается функция Foo ей дается другой ID. Однако, как вы можете наблюдать, это происходит циклично.
  • После 25-го вызова Foo(), как вы можете заметить, в пуле не осталось свободных потоков. С этой точки зрения, приложение как бы зависает до появления свободных потоков.
  • Как только освободждается очередной поток, программа сразу же забирает его и выполняет в нем Foo(). Это продолжается до тех пор, пока Foo() не выполнится 30 раз.
Пока мы не пошли дальше, я немного прокомментирую асинхронные вызовы методов:
  • Знайте, что ваш код выполняется в отдельном потоке и надо соблюдать некоторые меры безопасности. Этот топик о них, но обо всех я рассказывать не буду
  • Запомните, что пул потоков не бесконечный. Если вы планируете асинхронно вызывать множество функций и если они требуют довольно много времени на исполнение, то может произойти Thread Pool Starvation

BeginInvoke() и EndInvoke()

Итак, мы увидели, как вызвать метод без знания того, когда он выполнится. Но с EndInvoke() возможно сделать несколько вещей. EndInvoke производит блок, до тех пор, пока ваша функция не закончит выполнение.Поэтому вызов BeginInvoke вслед за EndInvoke – это всеравно что вызывать функцию в блокирующем режиме (потому что EndInvoke будет ожидать конца выполнения функции). Но как заставить .NET связать BeginInvoke и EndInvoke? На помощь нам приходит IAsyncResult. Когда вызывается BeginInvoke, он нам возвращает объект типа IAsyncResult – связку, позволяющую  .NET проследить выполнение вашей функции. Подуймайте о ней, как о метке, которая позволяет вам узнать о том, что происходит с вашей функцией. С помощью этой метки, вы можете узнать, когда ваша функция завершится, а так же вы сможете передать в нее некий объект. Итак, давайте рассмотрим пример. Для начала создадим новую Foo функцию.


private void FooOneSecond()
{
// подождем одну секунду
Thread.Sleep(1000);
}
private void UsingEndInvoke()
{
// создадим делегат, указывающий на нашу новую Foo
MethodInvoker simpleDelegate = new MethodInvoker(FooOneSecond);

// запустим FooOneSecond, но при этом 
// передадим ей  кое какую информацию
// обратите внимание на второй параметр
IAsyncResult tag =
simpleDelegate.BeginInvoke(null, "passing some state");

// программа блокируется до тех пор, пока FooOneSecond не завершится!
simpleDelegate.EndInvoke(tag);

// Как только EndInvoke завершится получим ее состояние
string strState = (string)tag.AsyncState;

// выведем это состояние
Trace.WriteLine("State When Calling EndInvoke: "
+ tag.AsyncState.ToString());
}

Что насчет исключений, которые могут произойти с ними?

Давайте немного запутаем нашу программу. Изменим FooOneSecond так, что бы он выбрасывал исключение. Сейчас вы должны были уже задаться вопросом, как отловить эти исключения? В BeginInvoke или же в EndInvoke? Так как же это сделать? Итак, это делается не в BeginInvoke. Задача BeginInvoke заключается просто в запуске функции в пуле потоков. Задача же EndInvoke – отчитаться о выполнении функции. Оно же и включает в себя исключения. Обратите внимание на этот кусочек кода:
private void FooOneSecond()
{
// засыпаем на одну секунду!
Thread.Sleep(1000);
// выбрасываем исключение
throw new Exception("Exception from FooOneSecond");
}
Давайте вызовем эту функцию и посмотрим, как мы можем перехватить исключение:
private void UsingEndInvoke()
{
// создадим делегат, указывающий на нашу новую Foo
MethodInvoker simpleDelegate =
new MethodInvoker(FooOneSecond);

// запустим FooOneSecond, но при этом 
// передадим ей  кое какую информацию
// обратите внимание на второй параметр
IAsyncResult tag = simpleDelegate.BeginInvoke(null, "passing some state");

try
{
// программа блокируется до тех пор, пока FooOneSecond не завершится!
simpleDelegate.EndInvoke(tag);
}
catch (Exception e)
{
// здесь мы можем поймать исключение
Trace.WriteLine(e.Message);
}

// Как только EndInvoke завершится получим ее состояние
string strState = (string)tag.AsyncState;

// выведем это состояние
Trace.WriteLine("State When Calling EndInvoke: "
+ tag.AsyncState.ToString());
}
После запуска программы, вы сможете увидеть, что исключение было поймано при вызове EndInvoke. Если вы решите вообще не использовать EndInvoke, то вы и не получите исключения. Однако, когда вы запускаете этот код с дебагером, настроенным на отлов исключений, ваш дебагер остановит программу. Но это дебагер. Не используя его и не вызывая EndInvoke, вы никогда не получите исключение.

Передача параметров в метод

Итак, вызов функции без параметров увел нас не слишком далеко, поэтому я предлагаю немного подправить наш код для того, что бы передать Foo несколько параметров.
private string FooWithParameters(string param1, int param2, ArrayList list)
{
// позволим изменить данные!
param1 = "Modify Value for param1";
param2 = 200;
list = new ArrayList();

return "Thank you for reading this article";
}
Теперь вызовем FooWithParametrs используя BeginInvoke и EndInvoke. Прежде чем что-то сделать, мы должны создать делегат, совпадающий с сигнатурой нашего метода.
public delegate string DelegateWithParameters(string param1, 
int param2, ArrayList list);
Думайте о BeginInvoke и EndInvoke как о разделении нашей функции на два разных метода. BeginInvoke ответственен за принятие всех параметров, следующих за двумя дополнительными параметрами, которые присущи каждому BeginInvoke (делегат обратного вызова и объекта состояния). EndInvoke ответственен за выходные параметры (которые промаркированы ref и out) и возвращение значения, если таковое присутствует. Давайте вернемся в наш пример и выясним, что является входным параметром, а что выходным. param1, param2 и list  - это все входные параметры, поэтому они должны быть переданы в качестве аргументов в BeginInvoke. Возвращаемый тип string – это выходной параметр, поэтому он должен быть возвращаемым типом EndInvoke. Самое прекрасное, что компилятор генерирует нужные сигнатуры для BeginInvoke и EndInvoke на основании вашего делегата. Заметьте, что я изменяю переменные моих входных параметров, для того, что бы выяснить, подтвердятся ли мои предположения о том, что будет без вызова BeginInvoke и EndInvoke. Я так же переопределяю ArrayList и передаю новый ArrayList Итак… попытайтесь отгадать, что же  произойдет…
private void CallFooWithParameters()
{
// создадим параметры для передачи функции
string strParam1 = "Param1";
int intValue = 100;
ArrayList list = new ArrayList();
list.Add("Item1");

// создадим делегат
DelegateWithParameters delFoo =
new DelegateWithParameters(FooWithParameters);

// вызовем BeginInvoke
IAsyncResult tag =
delFoo.BeginInvoke(strParam1, intValue, list, null, null);

// тут вам передастся управление и
// тут вы сможете сделать что хотите

// вызовем EndInvoke для получения возвращаемого значения
string strResult = delFoo.EndInvoke(tag);

// выведем эти параметры:
Trace.WriteLine("param1: " + strParam1);
Trace.WriteLine("param2: " + intValue);
Trace.WriteLine("ArrayList count: " + list.Count);
}
Давайте еще раз взглянем на FooWithParameters (для того, что бы вам не пришлось пролистывать):
private string FooWithParameters(string param1,
int param2, ArrayList list)
{
// изменим данные!
param1 = "Modify Value for param1";
param2 = 200;
list = new ArrayList();

return "Thank you for reading this article";
}
А вот те три строки, которые выдала мне программа после вызова EndInvoke():
param1: Param1
param2: 100
ArrayList count: 1
Давайте рассмотрим это. Даже тогда, когда моя функция изменила входные параметры, мы не увидели этих изменений. String – это изменяемый тип (mutable type), поэтому была создана копия строки и изменения не коснулись вызывающей стороны. Integer – это значимый тип (value type), поэтому была создана копия, когда передавалось значение. В конце концов, пересозданный ArrayList не вернется вызывающей стороне, так как ссылка на ArrayList была передана в качестве значения и, как факт, пересоздание ArrayList просто создаст новую область памяти для ArrayList, так называемой копией и будет передана. Т.е. эта ссылка потеряется и будет рассмотрена как утечка памяти (memory leak), но, к счастью для вас, сборщик мусора .NET позаботится о ней. Но что если мы хотим получить назад пересозданный ArrayList, как нам изменить наши параметры, что для этого нужно сделать? Это очень просто, мы просто помечаем наш параметр ArrayList словом ref. Давайте так же снабдим меткой out другой параметр и посмотрим, что нам покажет EndInvoke:
private string FooWithOutAndRefParameters(string param1,
out int param2, ref ArrayList list)
{
// изменяем данные!
param1 = "Modify Value for param1";
param2 = 200;
list = new ArrayList();

return "Thank you for reading this article";
}
Давайте посмотрим, что рассматривается в качестве выходного, а что в качестве входного параметров
  • Param1 – это входной параметр, он только принимается в BeginInvoke
  • Param2 – это входной и выходной параметр, он передается через BeginInvoke и EndInvoke (EndInvoke получает уже обновленное значение)
  • list – передается как ссылка, он тоже будет принят и BeginInvoke и EndInvoke.
Взглянем на наш делегат:
public delegate string DelegateWithOutAndRefParameters(string param1, 
out int param2, ref ArrayList list);
И, наконец, взглянем на функцию, вызывающую FooWithOutAndRefParameters:
private void CallFooWithOutAndRefParameters()
{
// создадим параметры для передачи функции
string strParam1 = "Param1";
int intValue = 100;
ArrayList list = new ArrayList();
list.Add("Item1");

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

// вызовем BeginInvoke
IAsyncResult tag =
delFoo.BeginInvoke(strParam1,
out intValue,
ref list,
null, null);

// тут вам передастся управление и
// тут вы сможете сделать что хотите

// вызовем EndInvoke помня, что числовое значение и list передаются
//как аргументы, поэтому они будут обновлены функцией
string strResult =
delFoo.EndInvoke(out intValue, ref list, tag);

// выведем параметры:
Trace.WriteLine("param1: " + strParam1);
Trace.WriteLine("param2: " + intValue);
Trace.WriteLine("ArrayList count: " + list.Count);
Trace.WriteLine("return value: " + strResult);
}
Вот что будет выведено:
param1: Param1
param2: 200
ArrayList count: 0
return value: Thank you for reading this article
Обратите внимание, что param1 не изменяется, так как он только входной параметр, а param2 был передан как выходной и был установлен в значение 200. Массив был переопределен и мы можем видеть указатель на новую ссылку, указывающую на нуль-элемент (zero element) (оригинальная ссылка была потеряна). Я надеюсь, что теперь вы поняли, как параметры передаются в BeginInvoke и EndInvoke. Давайте теперь перейдем к тому, как получить определить, что функция была выполнена.

Что вы должны знать о IAsyncResult

Вам должно понравиться, как EndInvoke возвращает выходные параметры и обновляет параметры с меткой ref, а так же, как он позволяет работать с исключениями, выбрасываемыми функциями. Для примера возьмем Foo, вызвав ее с помощью BeginInvoke, и, когда Foo выполнится мы спокойно вызовем EndInvoke. Но что если мы решим вызвать EndInvoke спустя, скажем, 20 минут после ее завершения? Обратите внимание, что EndInvoke все же отдаст вам то, что вы хотите (выходные и ref параметры), а так же выбросит исключения, если таковые имеются. Так где же вся эта информация хранится? Каким образом EndInvoke отдает информацию уже после того, как функция выполнилась? Ключ к этому – IAsyncResult. Я решил остановиться на этом пункте немного дольше, так как подозреваю, что этот объект хранит всю информацию, относительно вызванной функции. Обратите внимание, что EndInvoke принимает один параметр, который является объектом типа IAsyncResult. Этот объект содержит следующую информацию:
  • Завершилась ли функция?
  • Ссылку на делегат, использованную при вызове BeginInvoke
  • Все выходные параметры и их значения
  • Все ref параметры и их обновленные значения
  • Возвращенное значение (Return value)
  • Исключения, если они были вызваны
  • И т.д.
IAsyncResult выглядит невинным, но это всего лишь интерфейс с несколькими свойствами, но, фактически, это целый объект типа System.Runtime.Remoting.Messaging.AsyncResult.


Если мы капнем немного глубже, то найдем AsyncResult, содержащий объект _replyMsg типа System.Runtime.Remoting.Messaging.ReturnMessage. И что бы вы думали – это именно то, что мы искали.

Мы можем увидеть наше возвращенное значение, наши выходные параметры и ref параметры. Здесь так же находится поле с исключением. Обратите внимание, что я  развернул вкладку с OutArgs, где можно увидеть значение 200 и ссылку на пересозданный ArrayList. Вы так же можете увидеть свойство ReturnValue, строку “Thank you for reading this article”. Если бы у нас было исключение, EndInvoke выбросило бы его. Я думаю, что этого достаточно для вовода о том, что действительно вся информация о вашей функции хранится в таком маленьком объекте, как IAsyncResult, который вы получили из BeginInvoke. Это своеобразный ключ к вашим данным. Если вы потеряете этот объект, то вы уже никогда не узнаете ваши параметры и возвращенные значения. Вы так же не сможете поймать исключения, вызванные этим объектом. Это ключ и если вы потеряете его, то потеряете и всю информацию в дебрях .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;

// преобразуем в 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, если мы хотим получить нужные нам параметры. Обратите внимание, на то что мне пришлось сделать, что бы ее вызвать.
AsyncResult result = (AsyncResult)ar;
// получаем делегат
DelegateWithOutAndRefParameters del =
(DelegateWithOutAndRefParameters) result.AsyncDelegate;

Минуту. А в каком потоке вызывается делегат обратного вызова?

В конце концов, обратный вызов выполняется .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;

// преобразуем в 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 каждый раз выполняется в отдельном потоке. Все потоки из пула потоков. Заметьте так же, что обратный вызов так же выполняется трижды, соответственно и она так же находятся в пуле потоков. Что интересно, так это то, что обратный вызов выполняется в потоке с тем же ID, как и Foo. Поток 7 выполняет Foo, спустя 4 секунды, обратный вызов происходит в том же седьмом потоке. Аналогично для 12 и 13 потоков. Роль обратного вызова похожа на продолжение Foo. Я нажимаю мою кнопку много раз, хочу увидеть, может все-таки удастся мне увидеть разные потоки, но этого не происходит. Если вы подумали об этом, то, скорее всего, в этом есть смысл. Представьте себе, что .NET забирает поток для выполнения в нем Foo и затем забирает другой поток для обратного вызова. Это было бы очень расточительно. Не говоря уже о том, что в пуле потоков может быть очередь и вы должны были бы ожидать, пока потоки освободятся только для того, что бы сделать обратный вызов. Это было бы неправильно.

Использование паттерна Команда (Command Pattern) для наведения порядка.

Согласитесь, что все эти действия приводят к довольно большому беспорядку. У нас повсюду присутствуют BeginInvoke, EndInvoke, обратный вызов и т.д. Давайте попробуем применить паттерн Команда для наведения порядка при вызове. Использовать этот паттерн довольно просто. В основе всего лежит объект Command, который реализует просто интерфейс:
public interface ICommand
{
void Execute();
}
Самое время прекратить использовать нашу бесполезную функцию Foo и перейти к чему-то более реальному. Давайте создадим более естественный сценарий. Представьте, что у нас есть следующие пункты:
  • Форма, которая содержит таблицу отображающую заказчиков
  • Таблица обновляется используя критерии поиска по ID заказчика. Однако, база данных находится так далеко, что это занимает больше 5 секунд. Мы не можем себе позволить, что бы интерфейс пользователя зависал в этот момент, как если бы это было в однопоточном приложении
  • Наш объект устроен так, что он получает информацию по ID заказчика
Положим, что это наш бизнес уровень. Для нашего примера я написал код, который соответствует уровню данных.
public class BoCustomer
{
public DataSet GetCustomer(int intCustomerId)
{
// вызываем уровень данных и получаем информацию по ID
DataSet ds = new DataSet();
DataTable dt = new DataTable("Customer");
dt.Columns.Add("Id", typeof(int));
dt.Columns.Add("FirstName", typeof(string));
dt.Columns.Add("LastName", typeof(string));

dt.Rows.Add(intCustomerId, "Mike", "Peretz");
ds.Tables.Add(dt);

// это занимает некоторое время
System.Threading.Thread.Sleep(2000);
return ds;
}
}
А теперь создадим уже наш командный объект, который будет отвечать за обновление таблицы, используя ID заказчика:
public class GetCustomerByIdCommand : ICommand
{
private GetCustomerByIdDelegate m_invokeMe;
private DataGridView m_grid;
private int m_intCustmerId;
// Обратите внимание, что наш делегат помечет private
// поэтому пользоваться им может только "Команда"
private delegate DataSet GetCustomerByIdDelegate(int intCustId);

public GetCustomerByIdCommand(BoCustomer boCustomer,
DataGridView grid,
int intCustId)
{
m_grid = grid;
m_intCustmerId = intCustId;

// настраиваем на вызов делегата
m_invokeMe =
new GetCustomerByIdDelegate(boCustomer.GetCustomer);
}

public void Execute()
{
// вызываем метод в потоковом пуле
m_invokeMe.BeginInvoke(m_intCustmerId,
this.CallBack, // callback!
null);
}

private void CallBack(IAsyncResult ar)
{
// используем DataSet для вывода
DataSet ds = m_invokeMe.EndInvoke(ar);

// обновляем таблицу потоково-безопасным способом!
MethodInvoker updateGrid = delegate
{
m_grid.DataSource = ds.Tables[0];
};

if (m_grid.InvokeRequired)
m_grid.Invoke(updateGrid);
else
updateGrid();
}
}
Обратите внимание, что GetCustomerByIdCommand берет всю ему необходимую информацию для выполнения команды.
  • Таблицу для обновления
  • ID пользователя для поиска
  • Ссылку для бизнес уровня
Обратите внимание еще и на то, что делегат скрыт внутри объекта “Команда”, поэтому клиенту не нужно знать, как он работает внутри. Все клиенты должны просто создать “Команду” и вызвать Execute в ней. Как вы уже знаете, асинхронный вызов перенаправится в ThreadPool и, как вы могли догадаться, не совсем безопасно будет обновлять пользовательский интерфейс оттуда или из любого другого потока, отличного от того, в котором работает пользовательский интерфейс. Для решения этой проблемы мы скрыли реализацию в объекте “Команды” и проверяем значение таблицы InvokeRequired() на true. Если это так, то мы используем Control.Invoke для создания уверенности в том, что вызов будет идти в потоке пользовательского интерфейса (Обратите внимание, что я ипользую анонимные методы). Давайте теперь посмотрим, как на форме мы сможем создать этот объект и выполнить его.
private ICommand m_cmdGetCustById;
private void button1_Click(object sender, EventArgs e)
{
// получаем ID заказчика с экрана
int intCustId = Convert.ToInt32(m_txtCustId.Text);

// используем бизнес уровнь для получения данных
BoCustomer bo = new BoCustomer();

// создаем команду, которая имеет все средства для обновления Grid
m_cmdGetCustById = new GetCustomerByIdCommand(
bo, m_grid, intCustId);

// вызываем команду, не блокируя основной поток
m_cmdGetCustById.Execute();
}
Обратите внимание, что Execute() не блокирует поток. Но, прежде чем вы пойдете создавать миллионы команд, я должен вам напомнить следующее:
  • Использование этого паттерна может повредить ваш класс, поэтому используйте его мудро
  • В моем случае мне было легко создать базовый класс для команды, которая содержала в себе всю логику обновления Grid в потоково-безопасном стиле, но это только потому, что пример был простой
  • Обратите внимание, что делегат, BeginInvoke, EndInvoke, обратный вызов и весь прочий невероятный код для обновления пользовательского интерфейса инкапсулирован в мой Командный объект.

Выводы

Фух. Я потратил почти неделю на написание этой статьи. Я попытался охватить все важные аспекты вызова метода в деблокирующем режиме. Вы должны запомнить несколько вещей:
  • Делегаты должны содержать правильную сигнатуру для BeginInvoke и EndInvoke, вы должны ожидать все выходные параметры и исключения при вызове EndInvoke.
  • Не забывайте, что вы имеет дело с ThreadPool, когда используете BeginInvoke
  • Если вы используете обратный вызов, то хорошей мыслью будет использование Паттерна Команда для сокрытия всего того ужасного кода, который для этого потребуется
  • Пользовательский интерфейс должен блокироваться только тогда, когда это подразумевается разработчиком
Спасибо за чтение. Всего вам хорошего и будьте счастливы с .NET!
mikeperetz, 25 июля 2006
Оригинал на сайте www.codeproject.com

Комментариев нет:

Отправить комментарий

ваше мнение...