Асинхронный вызов метода - Что делает .NET на заднем плане

ОГЛАВЛЕНИЕ

Что делает .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, вы вообще не получите исключение.