Многопоточные приложения в Visual Studio.NET. Разработка приложения, демонстрирующего многопоточность для одного процессора

Автор работы: Пользователь скрыл имя, 09 Декабря 2013 в 22:08, курсовая работа

Описание работы


Современные операционные системы (OC) нацелены на наиболее эффективное использование ресурсов компьютера. По большей части эффективность достигается за счет разделения ресурсов компьютера между несколькими процессами. Многопоточность является естественным продолжением многозадачности, точно также как виртуальные машины, позволяющие запускать несколько ОС на одном компьютере, представляют собой логическое развитие концепции разделения ресурсов. В многопоточном приложении одновременно работает несколько потоков. Иногда вместо термина "поток" используется термин "нить". Потоки – это независимые друг от друга задачи, выполняемые в контексте процесса. Поток использует код и данные родительского процесса, но имеет свой собственный уникальный стек и состояние процессора, включающее указатель команд.

Содержание работы


РЕФЕРАТ…………………………………………………….…………
4
СПИСОК СОКРАЩЕНИЙ…………………………………………….
5
СОДЕРЖАНИЕ…………………………………………………………
6
ВВЕДЕНИЕ…………………………………………………………….
7
1 КОНЦЕПЦИЯ МНОГОПОТОЧНОСТИ……………………………
9
2 БИБЛИОТЕКА ПАРАЛЛЕЛЬНЫХ ЗАДАЧ (TPL) ………………..
11
2.1 Основные нововведения TPL……………………………………
11
2.2 Параллелизм данных……………………………………………
17
2.3 Параллелизм задач……………………………………………….
20
2.4 Потенциальные ошибки, связанные с параллелизмом

данных и задач……………………………………………………
21
3 СОЗДАНИЕ МНОГОПОТОЧНОГО ПРИЛОЖЕНИЯ В СРЕДЕ

VISUAL STUDIO.NET………………………………………………
23
3.1 Структурная схема программы …………………………………
23
3.2 Разработка и оптимизация кода программы на C#…………….
25
4 ТЕСТИРОВАНИЕ ПРИЛОЖЕНИЯ………………………………..
30
ЗАКЛЮЧЕНИЕ………………………………………………………..
35
Литература…………………………………………………………

Файлы: 1 файл

Курсяк C#.docx

— 192.86 Кб (Скачать файл)

Задачи имеют над потоками два  основных преимущества. Во-первых, это  более масштабируемое и эффективное  использование системных ресурсов. В фоновом режиме задачи помещаются в очередь ThreadPool, которая усовершенствована  с помощью специальных алгоритмов. Эти алгоритмы (поиск экстремума, перенос нагрузки и т.д.) определяют и настраивают количество потоков  так, чтобы наилучшим способом повысить производительность приложения. Во-вторых – больший программный контроль по сравнению с потоком. Задачи поддерживают такие API интерфейсы как ожидание, отмена, продолжение и многое другое.

Еще одним отличием класса Task от класса Thread является изменение подхода  к идентификации потока. Если для  класса Thread использовалось свойство Name, доступное как для записи, так  и для чтения, то в классе Task это  свойство отсутствует. Взамен ему было добавлено свойство Id, принадлежащее  типу int и доступное только для  чтения. Оно объявляется следующим  образом.

 

public int Id { get; }

 

 Свойство Id имеет ряд преимуществ  над свойством Name. Во-первых, если  объекту типа Thread не задать свойство Name вручную (задается присваиванием), то впоследствии поток останется  неименованным. Свойство же Id заполняется автоматически при создании задачи. Присваивание идентификатора задаче происходит динамически. Более того, программно недопустимы задачи с одинаковыми Id, чего нельзя сказать о свойстве Name, где именование разных потоков одинаковыми именами допустимо. Потоки с одинаковыми именами и неименованные потоки могу создать большую путаницу при построении многопоточной программы. Библиотека параллельных задач позволяет избежать данных неудобств.

 Также в класс Task было включено  свойство Result. Оно необходимо для  того, чтобы можно было организовать  возврат значения из задачи.

 

 public TResult Result { get; internal set; }

 

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

 Создание задачи и запуск  задачи происходит путем создания  объекта типа Task и вызова метода Start().

 

 public Task (Action act);

 

public void Start();

 

 Параметр act – точка входа в код, представляющий задачу. Подобным же образом происходит и создание потока посредством класса Thread. Но библиотека параллельных задач добавила несколько более эффективных методов создания задачи. Речь идет о методе StartNew(), определенном в классе TaskFactory.

 

public Task StartNew (Action act);

 

 Объект класса TaskFactory может быть получен из свойства Factory, доступного только для чтения в классе Task. В данном методе сначала создается экземпляр класса Task для действия, определяемого параметром act, а затем сразу же осуществляется запуск задачи на исполнение. Использование метода StartNew() является эффективным в тех случаях, когда задача создается и сразу же запускается, ведь в данной ситуации не нужно вызывать метод Start().

 

 В TPL (так же как и в  стандартных способах организации  параллелизма) в качестве задачи  можно использовать не только  метод, но и лямбда-выражение,  как отдельно решаемую задачу. Лямбда-выражения – особый вид  анонимных функций. Использование  лямбда-выражений наиболее полезно  в тех случаях, когда назначением  метода служит выполнение разовой  задачи (оформлять данную задачу  в отдельный метод было бы  избыточным).

 

Еще одним приятным нововведением  библиотеки параллельных задач является «семейство» методов ожидания. Если в классе Thread была определен метод Join(), ожидающий завершения потока, для  которого он был вызван, то в классе Task наипростейшим методом ожидания является метод Wait().

 

public void Wait();

 

 Разницы между функциями  Join() и Wait() нет абсолютно никакой  (разве что название wait наиболее  понятно описывает смысл функции). Но в библиотеку параллельных  задач включены и другие методы  ожидания, «родственные» методу Wait().

 

 public   static bool WaitAll (params Task[] tsk);

public   static int WaitAny (params Task[] tsk);

 

Метод WaitAll() ожидает завершения группы задач, и возврат из нее будет  произведен только тогда, когда будут  завершены все задачи. Метод WaitAny() ожидает завершения любой одной  задачи из указанных в параметре tsk. Если во время выполнения задача сама сгенерировала исключение, или  ее отменили, то будет сгенерирована  исключительная ситуация AggregateException. Но следует отметить, что указанные  в данной работе объявления функция Wait(), WaitAll(), WaitAny() далеко не единственные. Существует несколько вариантов  объявления данных методов, где в  параметрах можно указывать период простоя, отслеживать признак отмены (будет рассмотрен далее) и т.д. Данная группа методов обеспечивает довольно гибкую систему ожидания завершения задач и, существенно, облегчает  работу программиста, сокращая объем  кода многопоточного приложения.

Порой ресурсы, выделенные на поток, необходимо использовать до завершения программы, а в классе Thread «сборка мусора»  осуществляется только по завершении работы приложения. TPL решила данную задачу и позволила освобождать ресурсы  вручную. В классе Task реализован интерфейс IDisposable, в котором определяется метод Dispose().

 

public void Dispose();

 

 Этот метод освобождает ресурсы,  используемые классом Task. Но следует  иметь в виду, что метод Dispose() можно применить лишь к одной  задаче только после ее завершения. Если применить данную функцию  к активной задаче, то будет  сгенерировано исключение InvalidOperationException. Именно поэтому, во избежание  ошибок, эффективным является использование  метода Dispose() в связке с методами Wait() (или WaitAll()).

 Новаторской особенностью библиотеки  параллельных задач является  возможность создавать продолжение  задачи. Задача-продолжение автоматически  запустится после завершения  другой задачи. Создать такое  продолжение можно с помощью метода ContinueWith(), определенного в классе TaskFactory.

 

 public Task ContinueWith (Action cont_act);

 

 Этот механизм довольно удобен для запуска ряда последовательно выполняющихся задач. Более того, метод ContinueWith() устраняет необходимость ожидания завершения задачи, которая впоследствии была продолжена. Существует и другие методы, определенные в классе TaskFactory, которые позволяют более гибко использовать возможность продолжения задачи.

 

 public static Task ContinueWhenAny (Task [] tsk, Action< Task [] > cont_act);

 

public static Task ContinueWhenAll (Task [] tsk, Action< Task [] > cont_act);

 

 Метод ContinueWhenAny() запускает новую задачу, как только завершилась одна из указанных в параметре tsk. А метод ContinueWhenAll() создает и начинает исполнение задачи лишь тогда, когда завершилось исполнение всех задач, перечисленных в tsk.

 И последнее нововведение TPL, на которое хотелось бы обратить  внимание – это подсистема  отмены задач, основанная на  признаках отмены.

 Отмена задачи, как правило,  осуществляется следующим образом.  Сначала получается признак отмены  из источника признаков отмены, который представляет собой объект  класса CancellationTokenSourse (определенного  в пространстве имен System.Threading). Сразу  необходимо сделать замечание,  что после работы с источником  признаков отмены следует освободить  его ресурсы с помощью метода Dispose(). Сам же признак отмены  является экземпляром класса CancellationToken (так же определенном в пространстве  имен System.Threading).   Далее признак  передается задаче, которая должна  контролировать его на предмет запроса на отмену. Контроль осуществляется с помощью свойства IsCansellationRequested, доступного только для чтения.

 

public bool IsCansellationRequested { get; }

 

 Если данное свойство содержит  значение true, значит, поступил запрос  отмены, иначе – нет. Если же  запрос все-таки поступил, задача  должна быть завершена. Для  этого необходимо вызвать метод  ThrowIfCansellationRequested() для данного признака  отмены.

 

public void ThrowIfCansellationRequested();

 

Благодаря этому в отменяющем коде становится известно, что задача отменена. Для того чтобы удостовериться, что  задача действительно была отменена, можно использовать свойство IsCanseled, которое возвращает значение true в  случае, если задача отменена.

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

 

2.2 Параллелизм данных

 

Параллелизм данных заключается в  параллельной обработке некоторой  совокупности данных. Суть данного  подхода в том, что операция над  совокупностью данных (массив, коллекция  и т.п.) разбивается на несколько  потоков, в каждом из которых обрабатывается часть данных. Довольно легко заметить, что данный подход значительно ускоряет обработку данных, нежели последовательное взаимодействие. Нельзя сказать, что параллелизм данных не был возможен ранее средствами класса Thread. Возможность такой организации обработки данных была, однако она требовала немало усилий и времени. Библиотека TPL значительно упростила этот процесс.

 

Параллелизм данных в библиотеке параллельных задач осуществляется с помощью  методов For() и ForEach(), определенных в  классе Parallel.

Метод For() используется для того, чтобы  распределить на несколько процессоров (если такая возможность имеется) исполнение кода в цикле. Но следует  быть осторожным, так как использование  данного метода может как повысить, так и понизить производительность приложения. Понижение производительности будет происходить в тех случаях, когда будет произведена попытка  распределить мелкие циклы, или же когда  метод, исполняемый на каждом шаге цикла, тривиален. В этих случаях издержки распределения цикла по потокам  будут превышать сэкономленное  время, и пользы от такого использования  многопоточности не будет. Необходимо быть очень осторожным.

Существует несколько объявлений метода For().

 

public static ParallelLoopResult For (int from, int to, Action act);

 public static ParallelLoopResult For (int from, int to, Action act);

 

 Первым параметром передается начальное состояние переменной управления циклом. Второй параметр – значение, на единицу больше конечного. Параметр act – это тот метод (может быть как именованным, так и анонимным), который будет исполняться на каждом шаге цикла. В первом объявлении метод act должен принимать переменную типа int, через которую будет передаваться текущее значение переменной управления циклом. Во втором случае метод act принимает еще и переменную типа ParallelLoopState для организации прерывания цикла.

 Как видно из объявления  метода For(), данный метод возвращает  экземпляр объекта ParallelLoopResult. Для  объектов данного типа определенны  два свойства, которые доступны  только для чтения – IsCompleted и LowestBreakIteration.

 

 public bool IsCompleted { get; }

 

public Nulable LowestBreakIteration { get; }

 

 Свойство IsCompleted   принимает логическое значение true в том случае, если корректно выполнены все шаги цикла. Если же выполнение цикла прервалось раньше времени, данное свойство содержит значение false. Свойство LowestBreakIteration будет содержать наименьшее значение переменной управления циклом, если цикл был прерван.

 Преждевременное завершение  цикла For() осуществляется с помощью  метода Break(), определенного для объекта  типа ParallelLoopState, который передается  вторым параметром в метод  act соответствующего объявления  метода For().

 

public void Break();

 

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

Еще следует обратить внимание на тот факт, что при использовании  метода For() нельзя опираться на последовательность цикла. Если цикл выполнил 100 шагов, это  не означает, что эти 100 шагов соответствуют  первым 100 значениям переменной управления циклом.

 

 Метод ForEach() очень похож по  функциональности на метод For().

 

 public static ParallelLoopResult ForEach (IEnumerable data, Action act);

 

public static ParallelLoopResult ForEach (IEnumerable data, Action act);

 

 Он так же возвращает экземпляр объекта типа ParallelLoopResult, и данный цикл так же можно прервать с помощью метода Break() для экземпляра объекта типа ParallelLoopState, который передается в функцию act вторым параметром.

 

 Первым параметром метод  ForEach() принимает коллекцию данных, обрабатываемых в цикле, а вторым  – метод, выполняющийся на  каждом шаге цикла. Метод может  быть как именованным, так и  анонимным. Также следует обратить  внимание, что метод, передаваемый  через параметр act, принимает не  индекс обрабатываемого в цикле  массива (как это было при  использовании метода For()), а значение  или ссылку на каждый обрабатываемый  элемент.

 

 2.3 Параллелизм задач

 

Параллелизм задач обеспечивает параллельное выполнение двух или более независимых  задач. Параллелизм задач был  доступен и средствами класса Thread (они  описаны в работе Пономарёва А.А). Библиотека параллельных задач вносит ряд преимуществ в данный подход построения многопоточного приложения. Во-первых, TPL достаточно проста в применении. А во-вторых, использование библиотеки параллельных задач позволяет автоматически  масштабировать приложение на несколько  процессоров, без сложного управления потоками и задачами явным образом.

Класс Parallel, о котором уже упоминалось  ранее, содержит метод Invoke(), позволяющий  выполнять один или несколько  методов параллельно.

 

 public static void Invoke   (params Action[] acts);

 

 Каждый метод, который передается методу Invoke() не должен ни принимать, ни возвращать значение. Метод Invoke() сначала инициализирует выполнение, а потом ожидает завершения выполнения всех передаваемых ему методов. Это избавляет программиста от необходимости использовать метод Wait(), ведь функцию ожидания метод Invoke() берт на себя. Но с этим связан довольно весомый недостаток, ведь метод Invoke() обеспечивает параллельное выполнения лишь методов, указанных в параметрах, а вот метод-родитель приостанавливается. Следовательно, можно сделать вывод, что построить параллельное выполнение потока-родителя и дочернего потока с помощью метода Invoke() нельзя.

Информация о работе Многопоточные приложения в Visual Studio.NET. Разработка приложения, демонстрирующего многопоточность для одного процессора