Текст книги "C# 4.0: полное руководство"
Автор книги: Герберт Шилдт
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 72 (всего у книги 83 страниц)
В версии 4.0 среды .NET Framework внедрена новая подсистема, обеспечивающая структурированный, хотя и очень удобный способ отмены задачи. Эта новая подсистема основывается на понятии признака отмены. Признаки отмены поддерживаются в классе Task, среди прочего, с помощью фабричного метода StartNew()
.
–
ПРИМЕЧАНИЕ
Новую подсистему отмены можно применять и для отмены потоков, рассматривавшихся в предыдущей главе, но она полностью интегрирована в TPL и PLINQ. Именно поэтому эта подсистема рассматривается в этой главе.
–
Отмена задачи, как правило, выполняется следующим образом. Сначала получается признак отмены из источника признаков отмены. Затем этот признак передается задаче, после чего она должна контролировать его на предмет получения запроса на отмену. (Этот запрос может поступить только из источника признаков отмены.) Если получен запрос на отмену, задача должна завершиться. В одних случаях этого оказывается достаточно для простого прекращения задачи без каких-либо дополнительных действий, а в других – из задачи должен быть вызван метод ThrowIfCancellationRequested()
для признака отмены. Благодаря этому в отменяющем коде становится известно, что задача отменена. А теперь рассмотрим процесс отмены задачи более подробно.
'Признак отмены является экземпляром объекта типа CancellationToken
, т.е. структуры, определенной в пространстве имен System.Threading
. В структуре CancellationToken
определено несколько свойств и методов, но мы воспользуемся двумя из них. Во-первых, это доступное только для чтения свойство IsCancellationRequested
, которое объявляется следующим образом.
public bool IsCancellationRequested { get; }
Оно возвращает логическое значение true
, если отмена задачи была запрошена для вызывающего признака, а иначе – логическое значение false
. И во-вторых, это метод ThrowIfCancellationRequested()
, который объявляется следующим образом.
public void ThrowIfCancellationRequested()
Если признак отмены, для которого вызывается этот метод, получил запрос на отмену, то в данном методе генерируется исключение OperationCanceledException
. В противном случае никаких действий не выполняется. В отменяющем коде можно организовать отслеживание упомянутого исключения с целью убедиться в том, что отмена задачи действительно произошла. Как правило, с этой целью сначала перехватывается исключение AggregateException
, а затем его внутреннее исключение анализируется с помощью свойства InnerException
или InnerExceptions
. (Свойство InnerExceptions
представляет собой коллекцию исключений. Подробнее о коллекциях речь пойдет в главе 25.)
Признак отмены получается из источника признаков отмены, который представляет собой объект класса CancellationTokenSource
, определенного в пространстве имен System.Threading
. Для того чтобы получить данный признак, нужно создать сначала экземпляр объекта типа CancellationTokenSource
. (С этой целью можно воспользоваться вызываемым по умолчанию конструктором класса CancellationTokenSource
.) Признак отмены, связанный сданным источником, оказывается доступным через используемое только для чтения свойство Token, которое объявляется следующим образом.
public CancellationToken Token { get; }
Это и есть тот признак, который должен быть передан отменяемой задаче.
Для отмены в задаче должна быть получена копия признака отмены и организован контроль этого признака с целью отслеживать саму отмену. Такое отслеживание можно организовать тремя способами: опросом, методом обратного вызова и с помощью дескриптора ожидания. Проще всего организовать опрос, и поэтому здесь будет рассмотрен именно этот способ. С целью опроса в задаче проверяется упомянутое выше свойство IsCancellationRequested
признака отмены. Если это свойство содержит логическое значение true
, значит, отмена была запрошена, и задача должна быть завершена. Опрос может оказаться весьма эффективным, если организовать его правильно. Так, если задача содержит вложенные циклы, то проверка свойства IsCancellationRequested
во внешнем цикле зачастую дает лучший результат, чем его проверка на каждом шаге внутреннего цикла.
Для создания задачи, из которой вызывается метод ThrowIfCancellationRequested()
, когда она отменяется, обычно требуется передать признак отмены как самой задаче, так и конструктору класса Task
, будь то непосредственно или же косвенно через метод StartNew()
. Передача признака отмены самой задаче позволяет изменить состояние отменяемой задачи в запросе на отмену из внешнего кода. Далее будет использована следующая форма метода StartNew()
.
public Task StartNew(Action
CancellationToken признак_отмены)
В этой форме признак отмены передается через параметры, обозначаемые как состояние и признак_отмены. Это означает, что признак отмены будет передан как делегату, реализующему задачу, так и самому экземпляру объекта типа Task. Ниже приведена форма, поддерживающая делегат Action
.
public delegate void Actioncin T>(T obj)
В данном случае обобщенный параметр Т обозначает тип Object
. В силу этого объект obj должен быть приведен внутри задачи к типу CancellationToken
.
И еще одно замечание: по завершении работы с источником признаков отмены следует освободить его ресурсы, вызвав метод Dispose()
.
Факт отмены задачи может быть проверен самыми разными способами. Здесь применяется следующий подход: проверка значения свойства IsCanceled
для экземпляра объекта типа Task
. Если это логическое значение true
, то задача была отменена.
В приведенной ниже программе демонстрируется отмена задачи. В ней применяется опрос для контроля состояния признака отмены. Обратите внимание на то, что метод ThrowIfCancellationRequested()
вызывается после входа в метод MyTask()
. Это дает возможность завершить задачу, если она была отмена еще до ее запуска. Внутри цикла проверяется свойство IsCancellationRequested
. Если это свойство содержит логическое значение true
, а оно устанавливается после вызова метода Cancel()
для экземпляра источника признаков отмены, то на экран выводится сообщение об отмене и далее вызывается метод ThrowIfCancellationRequested()
для отмены задачи.
// Простой пример отмены задачи с использованием опроса.
using System;
using System.Threading;
using System.Threading.Tasks;
class DemoCancelTask {
// Метод, исполняемый как задача,
static void MyTask(Object ct) {
CancellationToken cancelTok = (CancellationToken) ct;
// Проверить, отменена ли задача,
// прежде чем запускать ее.
cancelTok.ThrowIfCancellationRequested();
Console.WriteLine(«MyTask() запущен»);
for(int count = 0; count < 10; count++) {
// В данном примере для отслеживания отмены задачи применяется опрос,
if(cancelTok.IsCancellationRequested) {
Console.WriteLine(«Получен запрос на отмену задачи.»);
cancelTok.ThrowIfCancellationRequested();
}
Thread.Sleep(500);
Console.WriteLine("В методе MyTask() подсчет равен " + count );
}
Console.WriteLine(«MyTask завершен»);
}
static void Main() {
Console.WriteLine(«Основной поток запущен.»);
// Создать объект источника признаков отмены.
CancellationTokenSource cancelTokSrc = new CancellationTokenSource();
// Запустить задачу, передав признак отмены ей самой и делегату.
Task tsk = Task.Factory.StartNew(MyTask, cancelTokSrc.Token,
cancelTokSrc.Token);
// Дать задаче возможность исполняться вплоть до ее отмены.
Thread.Sleep(2000);
try {
// Отменить задачу.
cancelTokSrc.Cancel();
// Приостановить выполнение метода Main() до тех пор,
// пока не завершится задача tsk.
tsk.Wait();
} catch (AggregateException exc) {
if(tsk.IsCanceled)
Console.WriteLine(«nЗадача tsk отменена»);
// Для просмотра исключения снять комментарии со следующей строки кода:
// Console.WriteLine(exc);
} finally {
tsk.Dispose();
cancelTokSrc.Dispose();
}
Console.WriteLine(«Основной поток завершен.»);
}
}
Ниже приведен результат выполнения этой программы. Обратите внимание на то что задача отменяется через 2 секунды.
Основной поток запущен.
MyTask() запущен
В методе MyTask() подсчет равен 0
В методе MyTask() подсчет равен 1
В методе MyTask() подсчет равен 2
В методе MyTask() подсчет равен 3
Получен запрос на отмену задачи.
Задача tsk отменена
Основной поток завершен.
Как следует из приведенного выше результата, выполнение метода MyTask()
отменяется в методе Main()
лишь две секунды спустя. Следовательно, в методе MyTask()
выполняются четыре шага цикла. Когда же перехватывается исключение AggregateException
, проверяется состояние задачи. Если задача tsk
отменена, что и должно произойти в данном примере, то об этом выводится соответствующее сообщение. Следует, однако, иметь в виду, что когда сообщение AggregateException
генерируется в ответ на отмену задачи, то это еще не свидетельствует об ошибке, а просто означает, что задача была отменена.
Выше были изложены лишь самые основные принципы, положенные в основу отмены задачи и генерирования исключения AggregateException
. Тем не менее эта тема намного обширнее и требует от вас самостоятельного и углубленного изучения, если вы действительно хотите создавать высокопроизводительные, масштабируемые приложения.
В предыдущих разделах был описан ряд понятий и основных способов организации и исполнения задач. Но имеются и другие полезные средства. В частности, задачи можно делать вложенными, когда одни задачи способны создавать другие, или же порожденными, когда вложенные задачи оказываются тесно связанными с создающей их задачей.
В предыдущем разделе было дано краткое описание исключения AggregateException
, но у него имеются также другие особенности, которые могут оказаться весьма полезными. К их числу относится метод Flatten()
, применяемый для преобразования любых внутренних исключений типа AggregateException
в единственное исключение AggregateException
. Другой метод, Handle()
, служит для обработки исключения, составляющего совокупное исключение AggregateException
.
При создании задачи имеется возможность указать различные дополнительные параметры, оказывающие влияние на особенности ее исполнения. Для этой цели указывается экземпляр объекта типа TaskCreationOptions
в конструкторе класса Task
или же в фабричном методе StartNew()
. Кроме того, в классе TaskFactory
доступно целое семейство методов FromAsync()
, поддерживающих модель асинхронного программирования (АРМ – Asynchronous Programming Model).
Как упоминалось ранее в этой главе, задачи планируются на исполнение экземпляром объекта класса TaskScheduler
. Как правило, для этой цели предоставляется планировщик, используемый по умолчанию в среде .NET Framework. Но этот планировщик может быть настроен под конкретные потребности разработчика. Кроме того, допускается применение специализированных планировщиков задач.
В примерах, приведенных до сих пор в этой главе, демонстрировались ситуации, в которых библиотека TPL использовалась таким же образом, как и класс Thread
. Но это было лишь самое элементарное ее применение, поскольку в TPL имеются и другие средства. К их числу относится класс Parallel
, который упрощает параллельное исполнение кода и предоставляет методы, рационализирующие оба вида параллелизма: данных и задач.
Класс Parallel
является статическим, и в нем определены методы For(), For Each()
и Invoke()
. У каждого из этих методов имеются различные формы. В частности, метод For()
выполняет распараллеливаемый цикл for
, а метод ForEach()
– распараллеливаемый цикл foreach
, и оба метода поддерживают параллелизм данных. А метод Invoke()
поддерживает параллельное выполнение двух методов или больше. Как станет ясно дальше, эти методы дают преимущество реализации на практике распространенных методик параллельного программирования, не прибегая к управлению задачами или потоками явным образом. В последующих разделах каждый из этих методов будет рассмотрен более подробно.
Распараллеливание задач методом Invoke()
Метод Invoke()
, определенный в классе Parallel
, позволяет выполнять один или несколько методов, указываемых в виде его аргументов. Он также масштабирует исполнение кода, используя доступные процессоры, если имеется такая возможность. Ниже приведена простейшая форма его объявления.
public static void Invoke(params Action[] actions)
Выполняемые методы должны быть совместимы с описанным ранее делегатом Action
. Напомним, что делегат Action
объявляется следующим образом.
public delegate void Action()
Следовательно, каждый метод, передаваемый методу Invoke()
в качестве аргумента, не должен ни принимать параметров, ни возвращать значение. Благодаря тому что параметр actions данного метода относится к типу params
, выполняемые методы могут быть указаны в виде переменного списка аргументов. Для этой цели можно также воспользоваться массивом объектов типа Action
, но зачастую оказывается проще указать список аргументов.
Метод Invoke()
сначала инициирует выполнение, а затем ожидает завершения всех передаваемых ему методов. Это, в частности, избавляет от необходимости (да и не позволяет) вызывать метод Wait()
. Все функции параллельного выполнения метод Wait()
берет на себя. И хотя это не гарантирует, что методы будут действительно выполняться параллельно, тем не менее, именно такое их выполнение предполагается, если система поддерживает несколько процессоров. Кроме того, отсутствует возможность указать порядок выполнения методов от первого и до последнего, и этот порядок не может быть таким же, как и в списке аргументов.
В приведенном ниже примере программы демонстрируется применение метода Invoke()
на практике. В этой программе два метода MyMeth()
и MyMeth2()
выполняются параллельно посредством вызова метода Invoke()
. Обратите внимание на простоту организации данного процесса.
// Применить метод Parallel.Invoke() для параллельного выполнения двух методов.
using System;
using System.Threading;
using System.Threading.Tasks;
class DemoParallel {
// Метод, исполняемый как задача,
static void MyMeth() {
Console.WriteLine(«MyMeth запущен»);
for (int count = 0; count < 5; count++) {
Thread.Sleep(500);
Console.WriteLine("В методе MyMeth подсчет равен " + count );
}
Console.WriteLine(«MyMeth завершен»);
}
// Метод, исполняемый как задача,
static void MyMeth2() {
Console.WriteLine(«MyMeth2 запущен»);
for(int count = 0; count < 5; count++) {
Thread.Sleep(500);
Console.WriteLine("В методе MyMeth2, подсчет равен " + count );
}
Console.WriteLine(«MyMeth2 завершен»);
}
static void Main() {
Console.WriteLine(«Основной поток запущен.»);
// Выполнить параллельно два именованных метода.
Parallel.Invoke(MyMeth, MyMeth2);
Console.WriteLine(«Основной поток завершен.»);
}
}
Выполнение этой программы может привести к следующему результату.
Основной поток запущен.
MyMeth запущен
MyMeth2 запущен
В методе MyMeth подсчет равен 0
В методе MyMeth2, подсчет равен 0
В методе MyMeth подсчет равен 1
В методе MyMeth2, подсчет равен 1
В методе MyMeth подсчет равен 2
В методе MyMeth2, подсчет равен 2
В методе MyMeth подсчет равен 3
В методе MyMeth2, подсчет равен 3
В методе MyMeth подсчет равен 4
MyMeth завершен
В методе MyMeth2, подсчет равен 4
MyMeth2 завершен
Основной поток завершен.
В данном примере особое внимание обращает на себя следующее обстоятельство: выполнение метода Main()
приостанавливается до тех пор, пока не произойдет возврат из метода Invoke(). Следовательно, метод Main(), в отличие от методов MyMeth()
и MyMeth2()
, не выполняется параллельно. Поэтому применять метод Invoke()
показанным здесь способом нельзя в том случае, если требуется, чтобы исполнение вызывающего потока продолжалось.
В приведенном выше примере использовались именованные методы, но для вызова метода Invoke()
это условие не является обязательным. Ниже приведен переделанный вариант той же самой программы, где в качестве аргументов в вызове метода Invoke()
применяются лямбда-выражения.
// Применить метод Parallel.Invoke()
//для параллельного выполнения двух методов.
// В этой версии программы применяются лямбда-выражения.
using System;
using System.Threading;
using System.Threading.Tasks;
class DemoParallel {
static void Main() {
Console.WriteLine(«Основной поток запущен.»);
// Выполнить два анонимных метода, указываемых в лямбда-выражениях.
Parallel.Invoke(() => {
Console.WriteLine(«Выражение #1 запущено»);
for(int count = 0; count < 5; count++) {
Thread.Sleep(500);
Console.WriteLine("В выражении #1 подсчет равен " + count );
}
Console.WriteLine(«Выражение #1 завершено»);
},
() => {
Console.WriteLine(«Выражение #2 запущено»);
for (int count = 0; count < 5; count++) {
Thread.Sleep(500);
Console.WriteLine("В выражении #2 подсчет равен " + count );
}
Console.WriteLine(«Выражение #1 завершено»);
} );
Console.WriteLine(«Основной поток завершен.»);
}
}
Эта программа дает результат, похожий на результат выполнения предыдущей программы.
Применение метода For()
В TPL параллелизм данных поддерживается, в частности, с помощью метода For()
, определенного в классе Parallel
. Этот метод существует в нескольких формах. Его рассмотрение мы начнем с самой простой формы, приведенной ниже:
public static ParallelLoopResult
For(int fromlnclusive, int toExclusive, Action
где fromlnclusive обозначает начальное значение того, что соответствует переменной управления циклом; оно называется также итерационным, или индексным, значением; a toExclusive — значение, на единицу больше конечного. На каждом шаге цикла переменная управления циклом увеличивается на единицу. Следовательно, цикл постепенно продвигается от начального значения fromlnclusive к конечному значению toExclusive минус единица. Циклически выполняемый код указывается методом, передаваемым через параметр body. Этот метод должен быть совместим с делегатом Action
, объявляемым следующим образом.
public delegate void Action
Для метода For()
обобщенный параметр Т должен быть, конечно, типа int
. Значение, передаваемое через параметр obj, будет следующим значением переменной управления циклом. А метод, передаваемый через параметр body, может быть именованным или анонимным. Метод For()
возвращает экземпляр объекта типа ParallelLoopResult
, описывающий состояние завершения цикла. Для простых циклов этим значением можно пренебречь. (Более подробно это значение будет рассмотрено несколько ниже.)
Главная особенность метода For()
состоит в том, что он позволяет, когда такая возможность имеется, распараллелить исполнение кода в цикле. А это, в свою очередь, может привести к повышению производительности. Например, процесс преобразования массива в цикле может быть разделен на части таким образом, чтобы разные части массива преобразовывались одновременно. Следует, однако, иметь в виду, что повышение производительности не гарантируется из-за отличий в количестве доступных процессоров в разных средах выполнения, а также из-за того, что распараллеливание мелких циклов может составить издержки, которые превышают сэкономленное время.
В приведенном ниже примере программы демонстрируется применение метода For()
на практике. В начале этой программы создается массив data
, состоящий из 1000000000 целых значений. Затем вызывается метод For()
, которому в качестве «тела» цикла передается метод MyTransfогm()
. Этот метод состоит из ряда операторов, выполняющих произвольные преобразования в массиве data
. Его назначение – сымитировать конкретную операцию. Как будет подробнее пояснено несколько ниже, выполняемая операция должна быть нетривиальной, чтобы параллелизм данных принес какой-то положительный эффект. В противном случае последовательное выполнение цикла может завершиться быстрее.
// Применить метод Parallel.For() для организации параллельно
// выполняемого цикла обработки данных.
using System;
using System.Threading.Tasks;
class DemoParallelFor {
static int[] data;
// Метод, служащий в качестве тела параллельно выполняемого цикла.
// Операторы этого цикла просто расходуют время ЦП для целей демонстрации,
static void MyTransform(int i) {
data[i] = data[i] / 10;
if(data[i] < 10000) data[i] = 0;
if(data[i] > 10000 & data[i] < 20000) data[i] = 100;
if(data[i] > 20000 & data[i] < 30000) data[i] = 200;
if(data[i] > 30000) data[i] = 300;
}
static void Main() {
Console.WriteLine(«Основной поток запущен.»);
data = new int[100000000];
// Инициализировать данные в обычном цикле for.
for (int i=0; i < data.Length; i++) data[i] = i;
// Распараллелить цикл методом For().
Parallel.For(0, data.Length, MyTransform);
Console.WriteLine(«Основной поток завершен.»);
}
}
Эта программа состоит из двух циклов. В первом, стандартном, цикле for инициализируется массив data
. А во втором цикле, выполняемом параллельно методом For()
, над каждым элементом массива data
производится преобразование. Как упоминалось выше, это преобразование носит произвольный характер и выбрано лишь для целей демонстрации. Метод For()
автоматически разбивает вызовы метода MyTransform()
на части для параллельной обработки отдельных порций данных, хранящихся в массиве. Следовательно, если запустить данную программу на компьютере с двумя доступными процессорами или больше, то цикл преобразования данных в массиве может быть выполнен методом For()
параллельно.
Следует, однако, иметь в виду, что далеко не все циклы могут выполняться эффективно, когда они распараллеливаются. Как правило, мелкие циклы, а также циклы, состоящие из очень простых операций, выполняются быстрее последовательным способом, чем параллельным. Именно поэтому цикл for
инициализации массива данных не распараллеливается методом For()
в рассматриваемой здесь программе. Распараллеливание мелких и очень простых циклов может оказаться неэффективным потому, что время, требующееся для организации параллельных задач, а также время, расходуемое на переключение контекста, превышает время, экономящееся благодаря параллелизму. В подтверждение этого факта в приведеннрм ниже примере программы создаются последовательный и параллельный варианты цикла for
, а для сравнения на экран выводится время выполнения каждого из них.
// Продемонстрировать отличия во времени последовательного
//и параллельного выполнения цикла for.
using System;
using System.Threading.Tasks;
using System.Diagnostics;
class DemoParallelFor {
static int[] data;
// Метод, служащий в качестве тела параллельно выполняемого цикла.
// Операторы этого цикла просто расходуют время ЦП для целей демонстрации,
static void MyTransform(int i) {
data[i] = data[i] / 10;
if(data[i] < 1000) data[i] = 0;
if(data[i] > 1000 & data[i] < 2000) data[i] = 100;
if(data[i] > 2000 & data[i] < 3000) data[i] = 200;
if(data[i] > 3000) data[i] = 300;
}
static void Main() {
Console.WriteLine(«Основной поток запущен.»);
// Create экземпляр объекта типа Stopwatch
// для хранения времени выполнения цикла.
Stopwatch sw = new Stopwatch();
data = new int[100000000];
// Инициализировать данные,
sw.Start();
// Параллельный вариант инициализации массива в цикле.
Parallel.For(0, data.Length, (i) => data[i] = i );
sw.Stop();
Console.WriteLine("Параллельно выполняемый цикл инициализации: " +
«{0} секунд», sw.Elapsed.TotalSeconds);
sw.Reset();
sw.Start();
// Последовательный вариант инициализации массива в цикле,
for(int i=0; i < data.Length; i++) data[i] = i;
sw.Stop();
Console.WriteLine("Последовательно выполняемый цикл инициализации: " +
«{0} секунд», sw.Elapsed.TotalSeconds);
Console.WriteLine();
// Выполнить преобразования,
sw.Start();
// Параллельный вариант преобразования данных в цикле.
Parallel.For(0, data.Length, MyTransform);
sw.Stop();
Console.WriteLine("Параллельно выполняемый цикл преобразования: " +
«{0} секунд», sw.Elapsed.TotalSeconds);
sw.Reset();
sw.Start();
// Последовательный вариант преобразования данных в цикле,
for(int i=0; i < data.Length; i++) MyTransform(i);
sw.Stop();
Console.WriteLine("Последовательно выполняемый цикл преобразования: " +
«{0} секунд», sw.Elapsed.TotalSeconds);
Console.WriteLine(«Основной поток завершен.»);
}
}
При выполнении этой программы на двухъядерном компьютере получается следующий результат.
Основной поток запущен.
Параллельно выполняемый цикл инициализации: 1.0537757 секунд
Последовательно выполняемый цикл инициализации: 0.3457628 секунд
Параллельно выполняемый цикл преобразования: 4.2246675 секунд
Последовательно выполняемый цикл преобразования: 5.3849959 секунд
Основной поток завершен.
Прежде всего, обратите внимание на то, что параллельный вариант цикла инициализации массива данных выполняется приблизительно в три раза медленнее, чем последовательный. Дело в том, что в данном случае на операцию присваивания расходуется так мало времени, что издержки на дополнительно организуемое распараллеливание превышают экономию, которую оно дает. Обратите далее внимание на то, что параллельный вариант цикла преобразования данных выполняется быстрее, чем последовательный. В данном случае экономия от распараллеливания с лихвой возмещает издержки на его дополнительную организацию.
–
ПРИМЕЧАНИЕ
Как правило, в отношении преимуществ, которые дает распараллеливание различных видов циклов, следует руководствоваться текущими рекомендациями корпорации Microsoft. Кроме того, необходимо убедиться в том, что распараллеливание цикла действительно приводит к повышению производительности, прежде чем использовать такой цикл в окончательно выпускаемом прикладном коде.
–
Что касается приведенной выше программы, то необходимо упомянуть о двух других ее особенностях. Во-первых, обратите внимание на то, что в параллельно выполняемом цикле для инициализации данных применяется лямбда-выражение, как показано ниже.
Parallel.For(0, data.Length, (i) => data[i] = i );
Здесь «тело» цикла указывается в лямбда-выражении. (Напомним, что в лямбда-выражении создается анонимный метод.) Следовательно, для параллельного выполнения методом For()
совсем не обязательно указывать именованный метод.
И во-вторых, обратите внимание на применение класса Stopwatch
для вычисления времени выполнения цикла. Этот класс находится в пространстве имен System.Diagnostics
. Для того чтобы воспользоваться им, достаточно создать экземпляр его объекта, а затем вызвать метод Start()
, начинающий отчет времени, и далее – метод Stop()
, завершающий отсчет времени. А с помощью метода Reset()
отсчет времени сбрасывается в исходное состояние. Продолжительность выполнения можно получить различными способами. В рассматриваемой здесь программе для этой цели использовано свойство Elapsed
, возвращающее объект типа TimeSpan
. С помощью этого объекта и свойства TotalSeconds
время отображается в секундах, включая и доли секунды. Как показывает пример рассматриваемой здесь программы, класс Stopwatch
оказывается весьма полезным при разработке параллельно исполняемого кода.
Как упоминалось выше, метод For()
возвращает экземпляр объекта типа ParallelLoopResult
. Это структура, в которой определяются два следующих свойства.
public bool IsCompleted { get; }
public Nullable
Свойство IsCompleted
будет иметь логическое значение true
, если выполнены все шаги цикла. Иными словами, при нормальном завершении цикла это свойство будет содержать логическое значение true
. Если же выполнение цикла прервется раньше времени, то данное свойство будет содержать логическое значение false
. Свойство LowestBreaklteration
будет содержать наименьшее значение переменной управления циклом, если цикл прервется раньше времени вызовом метода ParallelLoopState.Break()
.
Для доступа к объекту типа ParallelLoopState
следует использовать форму метода For()
, делегат которого принимает в качестве второго параметра текущее состояние цикла. Ниже эта форма метода For()
приведена в простейшем виде.
public static ParallelLoopResult For(int fromlnclusive, int toExclusive,
Action
В данной форме делегат Action
, описывающий тело цикла, определяется следующим образом.
public delegate void Action
Для метода For()
обобщенный параметр T1 должен быть типа int
, а обобщенный параметр Т2 – типа ParallelLoopState
. Всякий раз, когда делегат Action вызывается, текущее состояние цикла передается в качестве аргумента arg2.
Для преждевременного завершения цикла следует воспользоваться методом Break()
, вызываемым для экземпляра объекта типа ParallelLoopState
внутри тела цикла, определяемого параметром body. Метод Break()
объявляется следующим образом.
Вызов метода Break()
формирует запрос на как можно более раннее прекращение параллельно выполняемого цикла, что может произойти через несколько шагов цикла после вызова метода Break()
. Но все шаги цикла до вызова метода Break()
все же выполняются. Следует, также иметь в виду, что отдельные части цикла могут и не выполняться параллельно. Так, если выполнено 10 шагов цикла, то это еще не означает, что все эти 10 шагов представляют 10 первых значений переменной управления циклом.
Прерывание цикла, параллельно выполняемого методом For()
, нередко оказывается полезным при поиске данных. Так, если искомое значение найдено, то продолжать выполнение цикла нет никакой надобности. Прерывание цикла может оказаться полезным и в том случае, если во время очередной операции встретились недостоверные данные.