Урок 9. Ожидание завершения параллельных задач C#


На девятом уроке учебника по параллельному программированию в .NET мы узнаем, что параллельный характер задач означает, что невозможно предположить, что задачи завершены. Также мы поговорим о том, как синхронизировать задачи и фиксировать их необработанные исключения.

Синхронизация

При разработке программного обеспечения, использующего класс Tasks для включения параллельных операций, часто возникают ситуации, когда задача должна быть выполнена до того, как основной поток сможет продолжить обработку. Это может быть связано с тем, что параллельная задача генерирует результаты, которые необходимы позже в процессе, и вы должны ждать, пока результаты будут доступны, прежде чем пытаться их использовать. Поэтому параллельные задачи должны быть синхронизированы для правильной работы программного обеспечения.

Библиотека параллельных задач (TPL) включает в себя несколько методов, которые позволяют дождаться завершения одной или нескольких параллельных задач, прежде чем продолжить обработку. В этой статье мы рассмотрим три таких метода. Все они являются членами класса задач Tasks, который находится в System.Threading.Tasks, и мы также будем использовать класс Thread из System.Threading пространства имен, вы должны добавить следующие директивы using перед выполнением примеров.

using System.Threading;
using System.Threading.Tasks;
Метод Task.Wait

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

Как обсуждалось ранее в учебнике, планирование задач с помощью TPL довольно сложное. Это относится и к методу Wait. Хотя можно предположить, что основной поток блокируется, пока параллельная задача завершает свою работу, это не всегда так. Если имеется достаточно возможностей процессора и ожидаемая задача еще не запущена, она может быть передана в основной поток для обработки. Это может быть более эффективным, так как поток не нужно блокировать, и может означать, что задача запускается раньше, чем ожидалось.

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

При запуске программы вы увидите, что она выдает исключение ArgumentNullException. Это вызвано попыткой использовать массив до того, как он будет заполнен параллельной задачей.

int[] values = null;
 
Task loadDataTask = new Task(() =>
{
    Console.WriteLine("Loading data...");
    Thread.Sleep(5000);
    values = Enumerable.Range(1,10).ToArray();
});
loadDataTask.Start();
 
Console.WriteLine("Data total = {0}", values.Sum());    // ArgumentNullException 
Мы можем довольно легко решить проблему, вызвав метод Wait. При использовании без аргументов метод блокирует текущий поток до завершения задачи, независимо от того, сколько времени это займет:

int[] values = null;
 
Task loadDataTask = new Task(() =>
{
    Console.WriteLine("Loading data...");
    Thread.Sleep(5000);
    values = Enumerable.Range(1,10).ToArray();
});
loadDataTask.Start();
loadDataTask.Wait();
loadDataTask.Dispose();
 
Console.WriteLine("Data total = {0}", values.Sum());    // Data total = 55
Обработка исключений

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

Мы можем увидеть потерю исключения, добавив следующий код к основному методу проекта консольного приложения. Здесь мы запускаем задачу, которая показывает два сообщения, прежде чем выбросить исключение. Если вы запустите программу без отладки, то исключение InvalidOperationException вы никогда не увидите.

Task loadDataTask = new Task(() =>
{
    Console.WriteLine("Loading data...");
    Thread.Sleep(5000);
    Console.WriteLine("About to throw Exception");
    throw new InvalidOperationException();
});
loadDataTask.Start();
Console.ReadLine();
Необработанные исключения из задачи наблюдаются при обнаружении метода ожидания. Метод Wait блокирует текущий поток до завершения задачи. Если задача выполняется без ошибок, управление переходит к команде после вызова Wait. Если возникает необработанное исключение, оно оборачивается в AggregateException и выбрасывается в момент ожидания. Поэтому для обработки этих исключений следует обернуть вызов Wait в блок try/catch, который ловит AggregateExceptions.

Приведенный ниже обновленный пример добавляет обработку исключений к методу Wait. При перехвате AggregateException отображаются сообщения из его свойства InnerExceptions.

Task loadDataTask = new Task(() =>
{
    Console.WriteLine("Loading data...");
    Thread.Sleep(5000);
    throw new InvalidOperationException();
});
loadDataTask.Start();
 
try
{
    loadDataTask.Wait();
}
catch (AggregateException ex)
{
    foreach (var exception in ex.InnerExceptions)
    {
        Console.WriteLine(exception.Message);
    }
}
 
loadDataTask.Dispose();
 
/* OUTPUT
 
Loading data...
Operation is not valid due to the current state of the object.
 
*/
Добавление таймаутов

Если существует вероятность того, что задача будет выполняться слишком долго, вы можете указать значение таймаута, используя перегруженную версию метода Wait. Таймаут может быть представлен в виде целого числа, где он представляет собой число миллисекунд, или в виде значения TimeSpan. В любом случае значение -1 миллисекунды удаляет тайм - аут и блокирует основной поток на неопределенный срок.

Если задача завершается до истечения таймаута, основной поток разблокируется, как и следовало ожидать, и метод Wait возвращает true. Если задача не завершается вовремя, основной поток разблокируется, и метод возвращает false. Однако это не останавливает параллельную задачу, которая может быть выполнена позже. Если задача завершается с необработанным исключением после истечения таймаута, исключение может быть потеряно.

В приведенном ниже примере показано ожидание, используемое с таймаутом в десять секунд для задачи, которая выполняется примерно за половину этой продолжительности. Метод Wait возвращает true, и выполняется операция Sum.

int[] values = null;
 
Task loadDataTask = new Task(() =>
{
    Console.WriteLine("Loading data...");
    Thread.Sleep(5000);
    values = Enumerable.Range(1, 10).ToArray();
});
loadDataTask.Start();
 
if (loadDataTask.Wait(10000))
{
    Console.WriteLine("Data total = {0}", values.Sum());
    loadDataTask.Dispose();
}
else
{
    Console.WriteLine("Data read timeout");
}
  
/* OUTPUT
  
Loading data...
Data total = 55
 
*/
Следующий пример аналогичен, но использует значение TimeSpan для таймаута. В этом случае время ожидания слишком мало, чтобы позволить задаче завершиться вовремя.

int[] values = null;
 
Task loadDataTask = new Task(() =>
{
    Console.WriteLine("Loading data...");
    Thread.Sleep(5000);
    values = Enumerable.Range(1, 10).ToArray();
});
loadDataTask.Start();
 
if (loadDataTask.Wait(TimeSpan.FromSeconds(3)))
{
    Console.WriteLine("Data total = {0}", values.Sum());
    loadDataTask.Dispose();
}
else
{
    Console.WriteLine("Data read timeout");
}
 
/* OUTPUT
 
Loading data...
Data read timeout
 
*/ 
Примечание: в приведенных выше примерах метод Dispose был включен только тогда, когда задача завершается в течение периода ожидания. Удаление задачи по истечении времени ожидания может привести к возникновению исключения, поскольку незавершенная задача не может быть удалена. В этих случаях мы полагаемся на сборщика мусора, чтобы избавиться от задач позже.

Ожидание нескольких задач

Хотя можно использовать несколько вызовов Wait для ожидания выполнения нескольких параллельных задач, более элегантно использовать для этой цели один метод. Существует два метода, которые позволяют вам сделать это, каждый из которых является статическим членом класса задач и каждый принимает массив параметров задач, которые вы хотите дождаться, и необязательное значение таймаута. Первый из них - WaitAll. Как следует из названия, этот метод блокирует основной поток до тех пор, пока все задачи, переданные в его параметр, не будут завершены или не возникнут исключения.

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

Task loadUserDataTask = new Task(() =>
{
    Console.WriteLine("Loading User Data");
    Thread.Sleep(2000);
    Console.WriteLine("Employee User loaded");
});
 
Task loadCustomerDataTask = new Task(() =>
{
    Console.WriteLine("Loading Customer Data");
    Thread.Sleep(2000);
    Console.WriteLine("Employee Customer loaded");
});
 
loadUserDataTask.Start();
loadCustomerDataTask.Start();
 
Task.WaitAll(loadUserDataTask, loadCustomerDataTask);
 
loadUserDataTask.Dispose();
loadCustomerDataTask.Dispose();
 
Console.WriteLine("All data loaded");
 
/* OUTPUT
 
Loading Customer Data
Loading User Data
Employee Customer loaded
Employee User loaded
All data loaded
 
*/ 
Если вы запускаете несколько задач, но вам нужно выполнить только одну из них, прежде чем продолжить обработку, вы можете использовать метод WaitAny. Как и в случае WaitAll, задачи передаются в параметр массива метода. Обработка продолжается, как только заканчивается первая из задач. Это показано ниже, где две задачи имитируют проверку наличия двух серверов. Как только один из серверов найден, основной поток разблокируется.

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

Task server1Task = new Task(() =>
{
    Console.WriteLine("Checking Server 1");
    Thread.Sleep(4000);
    Console.WriteLine("Server 1 OK");
});
 
Task server2Task = new Task(() =>
{
    Console.WriteLine("Checking Server 2");
    Thread.Sleep(2000);
    Console.WriteLine("Server 2 OK");
});
 
server1Task.Start();
server2Task.Start();
 
Task.WaitAny(server1Task, server2Task);
 
Console.WriteLine("One server checked");
 
/* OUTPUT
 
Checking Server 1
Checking Server 2
Server 2 OK
One server checked
 
*/
Примечание: в этом примере задачи не удаляются, так как одна задача все еще будет выполняться, и вы не должны пытаться удалить не завершившиеся задачи.
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.

тегистатьи IT, параллельное программирование, си шарп, tasks, исключения




Отправляя сообщение я подтверждаю, что ознакомлен и согласен с политикой конфиденциальности данного сайта.




Разница между PHP и С#
700 человек в день
Биномиальное распределение с примерами