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