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

ОГЛАВЛЕНИЕ

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

Введение

В данной статье объясняются асинхронные вызовы методов и принципы их использования. После работы с делегатами, потоками и асинхронным вызовом настала пора поделиться знаниями по этому вопросу. Используются маленькие шаги и множество примеров. Вообще, будет показано, как вызывать методы асинхронно, как передавать параметры таким методам, и как узнать, когда метод завершает выполнение. Наконец, будет показан используемый шаблон команды для упрощения части кода. Большое преимущество асинхронного вызова метода .NET в том, что любой метод в проекте можно вызвать асинхронно без изменения кода метода. Важно знать множество хитростей, происходящих за кулисами внутри .NET, и они будут изучены в статье.

Синхронный против асинхронного

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

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

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

private void Foo()
{
    // ждать 10 секунд.
    Thread.Sleep(10000);
}

Как правило, когда приложение вызывает функцию Foo(), ему приходится ждать 10 секунд, пока Foo() не завершится и управление не вернется вызывающему потоку. Предположим, надо вызвать 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 раз, не дожидаясь завершения ни одного из вызовов функции. По сути, сделать нечто под названием выстрелить и забыть. Вызывается функция, ее не ждут и забывают о ней. И не меняется ни строки кода в сверхсложной затейливой функции 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. Для этого ничего не надо программировать, все происходит на заднем плане. Помните несколько вещей:
•    Foo() выполняется в отдельном потоке, принадлежащем пулу потоков .NET.
•    В пуле потоков .NET обычно находится 25 потоков (этот лимит меняется), и при каждом вызове Foo() она выполняется в одном из этих потоков. Нельзя контролировать, в каком именно.
•    Пул потоков имеет свои пределы! Когда все потоки используются, асинхронный вызов метода ставится в очередь до тех пор, пока один из потоков из пула не освободится. Это называется истощением пула потоков, и когда это происходит, снижается производительность.

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

Рассматривается пример истощения пула потоков. Изменяется функция Foo, чтобы ждала 30 секунд и сообщала следующее:
•    Число доступных потоков в пуле
•    Находится ли поток в пуле потоков
•    Идентификатор потока.

Сначала пул потоков содержит 25 потоков, поэтому функция Foo вызывается асинхронно 30 раз (чтобы увидеть, что произойдет после 25-го вызова).

private void CallFoo30AsyncTimes()
{
    // создать делегат MethodInvoker,
    // указывающий на функцию Foo.
    MethodInvoker simpleDelegate =
        new MethodInvoker(Foo);

    // вызов Foo асинхронно 30 раз.
    for (int i = 0; i < 30; i++)
    {
         // вызвать 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 назначается другой идентификатор потока. Но видно, что некоторые из потоков используются повторно.
•    После вызова Foo() 25 раз в пуле больше нет свободных потоков. В этот момент приложение ждет свободный поток.
•    Как только поток освобождается, программа сразу захватывает его, вызывая Foo(), и в пуле все еще 0 свободных потоков. Это продолжает происходить, пока Foo() не вызовется 30 раз.

Ниже делается несколько замечаний об асинхронном вызове методов.
•    Код будет выполняться в отдельном потоке, поэтому могут быть проблемы с потокобезопасностью. Это отдельная тема, не освещаемая здесь.
•    Помните, что пул имеет свои пределы. Если много функций вызывается асинхронно и если они долго выполняются, может произойти истощение пула потоков.
BeginInvoke() и EndInvoke()

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

private void FooOneSecond()
{
    // ждать одну секунду!
    Thread.Sleep(1000);
}

private void UsingEndInvoke()
{
    // создать делегат MethodInvoker, указывающий на функцию 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 – запустить функцию в ThreadPool. Задача EndInvoke – сообщить всю информацию о завершении функции, куда входят исключения. Посмотрите на следующий фрагмент кода:

private void FooOneSecond()
{
    // ждать одну секунду!
    Thread.Sleep(1000);
    // выбросить исключение
    throw new Exception("Exception from FooOneSecond");
}

Теперь вызывается FooOneSecond, и делается попытка перехватить исключение.

private void UsingEndInvoke()
{
    // создать делегат MethodInvoker, указывающий
    // на функцию Foo.
    MethodInvoker simpleDelegate =
        new MethodInvoker(FooOneSecond);

    // запустить FooOneSecond, но на этот раз передать ей некоторые данные!
    // look at the second parameter
    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";
}

Вызывается FooWithParameters с использованием 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);

    // обычно управление возвращается сразу,
    // поэтому тут можно сделать другую работу...

    // вызов end invoke для получения возвращаемого значения
    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

Надо разобраться во всем этом. Даже когда функция изменяет значения входных параметров, эти изменения не видны после вызова EndInvoke. Строка является изменяемым типом, поэтому создается копия строки, и изменение не передается обратно вызывающему оператору. Целые числа являются значимыми типами, и они создают копию при передаче по значению. Наконец, воссоздание ArrayList не возвращается вызывающему оператору, потому что ссылка на ArrayList передается по значению, и воссоздание ArrayList является созданием нового выделения для ArrayList с присвоением переданной "скопированной" ссылки. Ссылка теряется и обычно считается утечкой памяти; но, к счастью, сборщик мусора .NET в итоге уберет ее. Что если надо вернуть новый выделенный ArrayList и остальные изменения, внесенные в параметры? Что надо сделать? Следует пометить ArrayList как параметр ref. Еще добавляются выходные параметры, чтобы показать, как меняется 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);

    // обычно управление возвращается сразу,
    // поэтому тут можно сделать другую работу...

    // вызов end invoke. intValue и 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. Список массивов был перераспределен и теперь указывает на новую ссылку из нуля элементов (оригинальная ссылка утеряна). Теперь понятно, как передаются параметры посредством BeginInvoke и EndInvoke. Пора рассмотреть уведомление о завершении неблокирующей функции.


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


Использование шаблона команды ради аккуратности

Появляется путаница. Всюду есть BeginInvoke, EndInvoke и обратный вызов. Простой и удобный шаблон команды очищает вызов метода. Создается объект команды, реализующий следующий простой интерфейс:

public interface ICommand                    
{
    void Execute();
}

Пора перестать использовать везде бесполезную функцию Foo и сделать нечто более реальное. Создается более реалистичный сценарий. Скажем, имеется следующее:
•    Пользовательская форма, содержащая сетку, отображающую строки клиента.
•    Сетка обновляется строками на основе критерия поиска идентификатора клиента. Однако база данных расположена далеко, и получение набора данных клиента занимает 5 секунд; нельзя блокировать пользовательский интерфейс на время ожидания.
•    Имеется приятный бизнес-объект, получающий набор данных клиента на основе идентификатора клиента.

Допустим, это был слой бизнес-логики. Для упрощения примера жестко задано то, что обычно поступает из слоя данных.

public class BoCustomer
{
    public DataSet GetCustomer(int intCustomerId)
    {
        // вызвать слой данных и получить информацию и клиенте
        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;
    }
}

Создается команда, обновляющая сетку на основе идентификатора клиента.

public class GetCustomerByIdCommand : ICommand
{
    private GetCustomerByIdDelegate m_invokeMe;
    private DataGridView m_grid;
    private int m_intCustmerId;

    // делегат закрытый,
    // только команда может использовать его.
    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, // обратный вызов
            null);
    }

    private void CallBack(IAsyncResult ar)
    {
        // получить набор данных как вывод
        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 принимает всю информацию, требуемую ему для выполнения команды.
•    Обновляемая сетка.
•    Идентификатор клиента для поиска.
•    Ссылка на слой бизнес-логики.

Делегат спрятан внутри объекта команды, поэтому клиенту не нужно знать внутреннее устройство команды. Клиенту остается построить команду и вызвать Execute для нее. Асинхронный вызов методов производится в ThreadPool, и неразумно обновлять пользовательский интерфейс из ThreadPool или любого другого потока, отличного от потока пользовательского интерфейса. Для решения данной проблемы эта реализация прячется внутри команды, и проверяется на основе сетки, является ли InvokeRequired() истиной. Если это истина, с помощью Control.Invoke проверяется, что вызов направлен потоку пользовательского интерфейса. (Используются средства создания безымянных методов .NET 2.0.). Ниже показано, как форма создает команду и исполняет ее.

private ICommand m_cmdGetCustById;
private void button1_Click(object sender, EventArgs e)
{
    // получить идентификатор клиента с экрана
    int intCustId = Convert.ToInt32(m_txtCustId.Text);

    // использовать слой бизнес-логики для получения данных
    BoCustomer bo = new BoCustomer();

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

    // вызвать команду в неблокирующем режиме.
    m_cmdGetCustById.Execute();
}

Execute неблокирующий, но прежде чем создать массу классов команды, надо учесть следующее:
•    Шаблон команды может вызвать бурный рост числа классов, поэтому применяйте его разумно.
•    Было бы легко создать базовый класс для команды с логикой для потокобезопасного обновления сетки, но пример был оставлен простым.
•    Можно было бы передать TextBox в объект команды, чтобы он захватил ввод более динамично и позволил вызвать команду в любое время без ее воссоздания.
•    Делегат, BeginInvoke, EndInvoke, обратный вызов и код для обеспечения потокобезопасного обновления пользовательского интерфейса инкапсулированы в команду, что хорошо.

Заключение

В статье были рассмотрены все важные аспекты вызова метода в неблокирующем режиме. Надо помнить следующее:
•    Делегаты будут содержать правильную сигнатуру для BeginInvoke и EndInvoke. Все выходные параметры и исключения выходят при вызове EndInvoke.
•    При использовании BeginInvoke отнимаются ресурсы у ThreadPool, поэтому не перебарщивайте с этим!
•    При использовании обратного вызова сопровождающий его неприятный код скрывается с помощью шаблона команды.
•    Пользовательский интерфейс должен блокироваться только при работе с ним.