Управление асинхронными и параллельными задачами
Для разработчиков C# библиотека параллельных задач (TPL) упрощает написание параллельного кода. Однако не все коды подходят для параллелизации. Например, если цикл выполняет только небольшое количество работ по каждой итерации или не выполняется для многих итераций, то затраты на параллелизацию могут привести к более медленному выполнению кода. Кроме того, параллелизация, как и любой многопоточный код, упрощает выполнение программы.
Распространенные ошибки в параллелизме данных и задач
Во многих случаях Parallel.For и Parallel.ForEach могут значительно повысить производительность по сравнению с обычным выполнением последовательных циклов. Однако работа по параллелизации цикла представляет сложность, которая может привести к проблемам, которые не так распространены в последовательном коде.
Не предполагайте, что параллель всегда быстрее
В некоторых случаях параллельный цикл может выполняться медленнее, чем аналогичный последовательный. Основное практическое правило заключается в том, что параллельные циклы с небольшим числом итераций и быстро работающими делегатами пользователей вряд ли дадут заметное ускорение. Но в связи с тем, что на производительность влияет множество факторов, рекомендуем всегда оценивать фактические результаты.
Избегайте записи в области общей памяти
В последовательном коде нередко происходит чтение из или запись в статические переменные или поля класса. Однако всякий раз, когда несколько потоков обращаются к таким переменным одновременно, существует значительный потенциал для условий гонки. Несмотря на то что для синхронизации доступа к переменной можно использовать блокировки, связанные с нею затраты ресурсов могут снизить производительность. В связи с этим мы рекомендуем избегать или, по крайней мере, максимально ограничить обращение к общему состоянию в параллельном цикле. Для этого лучше всего использовать перегрузки Parallel.For и Parallel.ForEach, которые используют переменную System.Threading.ThreadLocal<T> для хранения локального состояния потока во время выполнения цикла.
Избегайте чрезмерной параллелизации
Использование параллельных циклов связано с чрезмерными затратами ресурсов на секционирование исходной коллекции и синхронизацию рабочих потоков. Преимущества параллелизации также ограничивает число процессоров на компьютере. Нет прироста скорости от запуска нескольких потоков, которые зависят от вычислений, на одном процессоре. В связи с этим излишней параллелизации цикла следует избегать.
Чаще всего излишняя параллелизация возникает во вложенных циклах. В большинстве случаев рекомендуется параллелизировать только внешний цикл, если только одно или несколько следующих условий не применяются:
- Известно, что внутренний цикл длинный.
- Вы выполняете дорогостоящие вычисления по каждому заказу.
- Целевая система, как известно, имеет достаточно процессоров для обработки количества потоков, создаваемых путем параллелизации обработки.
В любом случае лучший способ определения оптимальной формы запроса — это проверка и измерение.
Обработка исключений в асинхронных и параллельных задачах
При использовании параллельной библиотеки задач (TPL) для выполнения задач исключения могут выполняться различными способами. Чаще всего задача вызывает исключение. Исключение может происходить, когда задача выполняется в пуле потоков или когда она выполняется в основном потоке. В любом случае исключение распространяется обратно в вызывающий поток.
При использовании Task.Wait метода для ожидания завершения задачи все исключения, которые были вызваны задачей, распространяются обратно в вызывающий поток. Эти исключения можно обрабатывать с помощью блока try/catch. Если задача является родительской задачей для прикрепленных дочерних задач, или если вы ожидаете выполнения нескольких задач, может возникнуть несколько исключений. Если выбрасываются одно или несколько исключений, они упаковываются в экземпляр AggregateException.
Исключение AggregateException имеет InnerExceptions свойство, которое можно перечислить для проверки всех исходных исключений, которые были созданы, и обрабатывать (или не обрабатывать) каждый из них по отдельности.
В следующем примере показано, как обрабатывать исключения, создаваемые задачей.
public static partial class Program
{
public static void Main()
{
HandleThree();
}
public static void HandleThree()
{
var task = Task.Run(
() => throw new CustomException("This exception is expected!"));
try
{
task.Wait();
}
catch (AggregateException ae)
{
foreach (var ex in ae.InnerExceptions)
{
// Handle the custom exception.
if (ex is CustomException)
{
Console.WriteLine(ex.Message);
}
// Rethrow any other exception.
else
{
throw ex;
}
}
}
}
}
// Define the CustomException class
public class CustomException : Exception
{
public CustomException(string message) : base(message) { }
}
// The example displays the following output:
// This exception is expected!
В этом примере HandleThree метод создает задачу, которая вызывает CustomExceptionисключение. Блок try/catch перехватывает AggregateException и выполняет итерацию по коллекции InnerExceptions. Если исключение имеет тип CustomException, оно выводит сообщение в консоль. Если это любой другой тип исключения, он перебросит его.
Вы также можете обрабатывать исходные исключения с помощью AggregateException.Handle метода. Этот метод принимает делегат, который вызывается для каждого исключения в InnerExceptions коллекции. Если делегат возвращает значение true, то исключение считается обработанным и удаляется из коллекции. Если он возвращает значение false, исключение повторно выбрасывается.
В следующем примере показано, как использовать Handle метод для обработки исключений, создаваемых задачей.
public static partial class Program
{
public static void HandleFour()
{
var task = Task.Run(
() => throw new CustomException("This exception is expected!"));
try
{
task.Wait();
}
catch (AggregateException ae)
{
ae.Handle(ex =>
{
// Handle the custom exception.
if (ex is CustomException)
{
Console.WriteLine(ex.Message);
return true;
}
// Rethrow any other exception.
return false;
});
}
}
}
В этом примере HandleFour метод создает задачу, которая вызывает CustomExceptionисключение. Блок try/catch перехватывает AggregateException и вызывает метод Handle. Делегат проверяет, является ли исключение типом CustomException. Если исключение имеет тип CustomException, делегат выводит сообщение в консоль и возвращает true. Ответ true указывает, что исключение обработано. Если исключение является любым другим типом исключения, делегат возвращает false, что приводит к повторному выбросу исключения.
Сводка
В этом уроке описываются ситуации, когда код не подходит для параллелизации и обсуждает распространенные ошибки в данных и параллелизме задач. Например, если считать, что параллельная обработка всегда быстрее, выполнение записей в общую память и чрезмерная параллелизация. В содержимом также объясняется, как обрабатывать исключения в асинхронных и параллельных задачах, включая использование Task.Wait метода и AggregateException.Handle метода.
Ключевые моменты
- Не все коды подходят для параллелизации. Тестирование и измерение производительности важно перед параллелизированием кода.
- Распространенные ловушки в данных и параллелизме задач включают в себя предположение, что параллелизм всегда быстрее, запись в общую память и перепараллеливание.
- Исключения в асинхронных и параллельных задачах можно обрабатывать с помощью
Task.Waitметода иAggregateException.Handleметода. - Исключение
AggregateExceptionимеетInnerExceptionsсвойство, которое можно перечислить для проверки всех исходных исключений, которые были созданы.