Разработка приложения, демонстрирующего многопоточность для одного процессора

Технология создания многопоточных приложений в современных системах программирования с использованием языка C# в Visual Studio.NET. Разработка алгоритма и структуры программы. Описание и особенности тестирования приложения с разным количеством потоков.

Рубрика Программирование, компьютеры и кибернетика
Вид курсовая работа
Язык русский
Дата добавления 14.03.2013
Размер файла 773,0 K

Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже

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

Размещено на http://www.allbest.ru/

37

Размещено на http://www.allbest.ru/

Курсовой проект

по дисциплине «Программирование на С/С++»

Тема

Разработка приложения, демонстрирующего многопоточность для одного процессора

Разработал студент

А.В. Гулевский

РЕФЕРАТ

Пояснительная записка с., рисунков, источников, приложение.

Ключевые слова: VISUAL STUDIO .NET, C++, С#, МНОГОПОТОЧНОСТЬ, TRADE.

Объект исследования или разработки - многопоточное приложение, построенное в среде Visual Studio .NET.

Цель работы - изучить технологию создания многопоточных приложений в современных системах программирования.

Метод исследования и аппаратура - интегрированная среда разработки приложений Visual Studio .NET, программное моделирование на ЭВМ.

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

Основные конструктивные, технологические и технико-эксплуатационные характеристики - персональная ЭВМ с емкостью оперативной памяти не меньше 512 Мб, оснащенная Windows XP и выше.

Степень внедрения - применена технология разработки многопоточных приложений в среде Visual Studio .NET, создан программный продукт.

СПИСОК СОКРАЩЕНИЙ

OC - Операционная система

API - application programming interface

TPL - System Threading Tasks

HTML - HyperText Markup Language

URL - Uniform Resource Locator

СОДЕРЖАНИЕ

ВВЕДЕНИЕ

1. КОНЦЕПЦИЯ МНОГОПОТОЧНОСТИ

2. БИБЛИОТЕКА ПАРАЛЛЕЛЬНЫХ ЗАДАЧ (TPL)

2.1 Основные нововведения TPL

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

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

2.4 Потенциальные ошибки, связанные с параллелизмом данных и задач

3. СОЗДАНИЕ МНОГОПОТОЧНОГО ПРИЛОЖЕНИЯ В СРЕДЕ VISUAL STUDIO.NET

3.1 Структурная схема программы

3.2 Разработка и оптимизация кода программы на C#

4. ТЕСТИРОВАНИЕ ПРИЛОЖЕНИЯ

ЗАКЛЮЧЕНИЕ

Литература

Приложения

ВВЕДЕНИЕ

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

Потоки часто становятся источниками программных ошибок особого рода. Эти ошибки возникают при использовании потоками разделяемых ресурсов системы (например, общего адресного пространства) и являются частным случаем более широкого класса ошибок - ошибок синхронизации. Если задача разделена между независимыми процессами, то доступом к их общим ресурсам управляет операционная система и вероятность ошибок из-за конфликтов доступа снижается. Важнейшее преимущество потоков перед самостоятельными процессами заключается в том, что накладные расходы на создание нового потока в многопоточном приложении оказываются ниже, чем накладные расходы на создание нового самостоятельного процесса. Уровень контроля над потоками в многопоточном приложении выше, чем уровень контроля приложения над дочерними процессами. Кроме того, многопоточные программы не склонны оставлять за собой вереницы "осиротевших" независимых процессов. Для порождения дочернего процесса, находясь в родительской программе, поток получает копии виртуальной памяти дескрипторов файлов и пр. Модификации содержимого памяти, закрытие файлов и другие подобные действия в дочернем процесса не влияют на работу родительского процесса и наоборот. С другой стороны, когда программа создает поток, ничего не копируется. Оба потока - старый и новый - имеют доступ к общему виртуальному пространству, общим дескрипторам файлов и другим системным ресурсам. Если, к примеру, один поток меняет значение переменной, это изменение отражается на другом потоке. Точно так же, когда один поток закрывает файл, второй поток теряет возможность работать с этим файлом.

1. КОНЦЕПЦИЯ МНОГОПОТОЧНОСТИ

Фундаментальными понятиями, связанными с многопоточным программированием, являются «процесс» (process) и «поток» (thread). Для эффективного многопоточного программирования необходимо довольно хорошо видеть разницу между данными терминами.

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

Другим фундаментальным понятием является поток. Под потоком понимают некую сущность внутри процесса, отвечающую за исполнение кода, содержащегося в адресном пространстве процесса. В каждом процессе есть минимум один поток (первичный), который автоматически создается системой в момент возникновения процесса. Далее первичный поток может порождать другие потоки, а те в свою очередь новые и так далее.

Потоки, работающие параллельно в одном процессе, разделяют не только адресное пространство, но и открытые файлы, и множество других ресурсов. Процессы же, исполняющиеся параллельно, совместно используют физическую память, диски и другие ресурсы. Нетрудно заметить некую аналогию в работе процессов и потоков. Более того, потоки унаследовали некоторые свойства процессов, из-за чего их довольно часто называют «упрощенные процессы». В связи с существованием потоков появился термин многопоточность. Многопоточносью называют свойство платформы или приложения, заключающееся в том, что порожденный в операционной системе процесс может состоять из нескольких потоков, выполняющихся без предписанного порядка во времени, т.е. параллельно.

Необходимо сделать оговорку, что многопоточность в различных системах реализуется не только конкретными API-интерфейсами, но и самими подходами к ее организации. В рамках данной курсовой работы будет рассматриваться многопоточность исключительно в Windows-системах.

Конечно же, один процессор может исполнять только один поток инструкций, и именно поэтому «подлинная» многопоточность, когда потоки команд исполняются действительно параллельно и независимо, в чистом виде практически нигде не встречается. Как правило, число процессоров значительно меньше числа потоков в системе. Из-за этого операционной системе приходится эмулировать параллельную работу процессоров. Такая эмуляция называется псевдопараллелизмом или псевдомноготочностью. В этом случае потоки инструкций не выполняются параллельно. Время работы процессора разбивается на небольшие интервалы, которые называются квантами. Процессор выделяет кванты на каждый поток поочередно (распределение происходит системно, и программист может влиять на это распределение лишь косвенным образом) и быстро переключается между потоками, создавая иллюзию многопоточности. При таких переключениях, выполнение текущего потока приостанавливается, а все содержимое регистров процессора сохраняется в специальной области памяти. Когда очередь вернется к этому потоку, содержимое регистров полностью восстановится и работа когда-то остановленного потока продолжится, словно она и не прерывалась.

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

2. БИБЛИОТЕКА ПАРАЛЛЕЛЬНЫХ ЗАДАЧ (TPL)

Как уже упоминалось ранее, одним из наиболее важных средств, внедренных в версию 4.0 среды .NET Framework, является библиотека параллельных задач или Task Parallel Library (или TPL). Эта библиотека вносит немало изменений, касающихся параллельного программирования. Данные изменения касаются как основ организации многопоточности в C#, так и «поверхностных» нововведений, таких как изменение или введение новых методов, которые значительно упрощают процесс создания многопоточного приложения.

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

2.1 Основные нововведения TPL

Библиотека TPL определена в пространстве имен System.Threading.Tasks.

Следует обратить внимание, что очень часто в программу включают и класс System.Threading. Это обусловлено тем, что использование библиотеки TPL для организации многопоточной обработки является рекомендуемым, но не обязательным. Построение многопотчного приложения с использованием класса Thread по-прежнему находит широкое применение. Более того, библиотека параллельных задач имеет ряд существенных недостатков, о которых в дальнейшем будет рассказано более подробно. Эти недостатки могут быть устранены стандартными методами организации многопоточности.

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

Задачи имеют над потоками два основных преимущества. Во-первых, это более масштабируемое и эффективное использование системных ресурсов. В фоновом режиме задачи помещаются в очередь 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() нельзя.

Однако есть еще один значительный недостаток. В использовании данного подхода нельзя указать порядок выполнения методов. Последовательность, в которой методы передаются в Invoke(), вовсе не определяет порядок их выполнения. К тому же TPL не поддерживает возможность явного указания приоритетов задач (они присваиваются автоматически планировщиком задач среды .NET Framework). Это является очень существенным недостатком библиотеки параллельных задач. Тот факт, что управление потоками полностью контролируется планировщиком задач, с одной стороны, значительно облегчает работу разработчика программного обеспечения, а с другой, - делает систему управления потоками и задачами менее гибкой. Возможно именно поэтому, построение многопоточных приложений на основе класса Thread остается по-прежнему популярным.

2.4 Потенциальные ошибки, связанные с параллелизмом данных и задач

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

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

- Используя метод Parallel.Invoke(), следует помнить, что функция ожидания завершения задачи уже включена в данный метод, и использование методов ожидания в данной ситуации нежелательно, а порой и недопустимо;

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

- В .NET Framework большая часть статических методов потокобезопасна, следовательно, они могут быть вызваны из нескольких параллельно выполняющихся потоков одновременно. Но следует обратить внимание, что действующая в данных ситуациях синхронизация значительно замедлит исполнение программы. Следует учитывать данные издержки;

- Параллельное использование потокоопасных методов может привести к повреждению или потере данных (иногда это остается незаметным, но потеря данных все равно происходит). Следует стараться избегать использования данных методов одновременно несколькими потоками;

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

3. СОЗДАНИЕ МНОГОПОТОЧНОГО ПРИЛОЖЕНИЯ В СРЕДЕ VISUAL STUDIO.NET

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

3.1 Структурная схема программы

На рисунке 1 показана структурная схема нашего приложения.

Рисунок 1 - Структурная схема приложения downloader

3.2 Разработка и оптимизация кода программы на C#

Итак, поток - это последовательность команд программы, которая выполняется параллельно с другими потоками. Следует отметить две особенности потоков - во-первых они могут использовать один и тот же программный код, и во-вторых они имеют доступ к одним и тем же данным.

В качестве данных у нас будет очередь из URL адресов

URLs.Enqueue("http://google.com");

URLs.Enqueue("http://ya.ru");

URLs.Enqueue("http://vorstu.ru");

URLs.Enqueue("http://mail.ru");

URLs.Enqueue("http://ru.akinator.com");

URLs.Enqueue("http://kinopoisk.ru");

URLs.Enqueue("http://internat.ax3.net");

URLs.Enqueue("http://kaspersky.ru");

Общий код - это код для загрузки очередной web страницы:

WebRequest request = WebRequest.Create(URL);

HttpWebResponse response = (HttpWebResponse)request.GetResponse();

string HTML = (new StreamReader(response.GetResponseStream())).ReadToEnd();

Синхронизацию легче всего делать с помощью оператора lock:

Object locker = new Object();

void CriticalMethod()

{

lock(locker)

{

//критическая секция

//здесь мы работаем с общими для потоков данными

}

}

Оператор lock принимает как аргумент объект синхронизации locker и допускает внутрь критической секции только один поток, остальные потоки подошедшие к lock - ожидают пока завершится поток, находящийся внутри критической секции. В качестве объекта синхронизации может выступать любой созданный (не null) объект ссылочного типа.

Первый вариант рабочей программы выглядит так:

using System;

using System.Collections.Generic;

using System.Text;

using System.Threading;

using System.Net;

using System.IO;

namespace Downloader

{

class Program

{

//очередь адресов для закачки

static Queue<string> URLs = new Queue<string>();

//список скачанных страниц

static List<string> HTMLs = new List<string>();

//локер для очереди адресов

static object URLlocker = new object();

//локер для списка скачанных страниц

static object HTMLlocker = new object();

static void Main(string[] args)

{

URLs.Enqueue("http://microsoft.com");

URLs.Enqueue("http://google.com");

URLs.Enqueue("http://ya.ru");

//создаем и запускаем 3 потока

for (int i = 0; i < 3;i++)

(new Thread(new ThreadStart(Download))).Start();

//ожидаем нажатия Enter

Console.ReadLine();

}

public static void Download()

{

//будем крутить цикл, пока не закончатся ULR в очереди

while (true)

{

string URL;

//блокируем очередь URL и достаем оттуда один адрес

lock (URLlocker)

{

if (URLs.Count == 0)

break;//адресов больше нет, выходим из метода, завершаем поток

else

URL = URLs.Dequeue();

}

Console.WriteLine(URL + " - start downloading ...");

//скачиваем страницу

WebRequest request = WebRequest.Create(URL);

HttpWebResponse response = (HttpWebResponse)request.GetResponse();

string HTML = (new StreamReader(response.GetResponseStream())).ReadTo End();

//блокируем список скачанных страниц, и заносим туда свою страницу

lock (HTMLlocker)

HTMLs.Add(HTML);

//

Console.WriteLine(URL + " - downloaded (" + HTML.Length+" bytes)");

}

}

}

}

Этот пример - рабочий, но в нем нет таймера, выбора потоков, обработки ошибок и контроля завершения потоков.

В приведенном примере мы создали три потока. Стартовым методом для каждого потока был метод Download(). Поток существует до тех пор, пока не выполнится метод Download(). Как только этот метод завершается, завершается и поток. В этом примере главный поток (тот, который создает остальные потоки) не ожидает завершения дочерних потоков и не отслеживает их завершения. Как только пользователь нажмет Enter в точке Console.ReadLine();), главный поток завершится, а дочерние будут продолжать работать, пока не будет исчерпана очередь адресов. Приложение завершится только тогда, когда завершатся все его потоки.

Для того, чтобы сделать ожидание завершения множества потоков из главного потока создаем массив флажков, по одному флажку на поток. Когда поток отработал - он устанавливает свой флажок. Главный поток в это время ожидает, пока все флажки будут установлены - и когда это происходит - продолжает свое выполнение. Для нашего примера программы скачивания сайтов это будет выглядеть так:

static void Main(string[] args)

{

URLs.Enqueue("http://microsoft.com");

URLs.Enqueue("http://google.com");

URLs.Enqueue("http://ya.ru");

//создаем массив хендлеров, для контроля завершения потоков

ManualResetEvent[] handles = new ManualResetEvent[3];

//создаем и запускаем 3 потока

for (int i = 0; i < 3; i++)

{

handles[i] = new ManualResetEvent(false);

(new Thread(new ParameterizedThreadStart(Download))).Start(handles [i]);

}

//ожидаем, пока все потоки отработают

WaitHandle.WaitAll(handles);

//

Console.WriteLine("Download completed");

Console.ReadLine();

}

public static void Download(object handle)

{

//будем крутить цикл, пока не закончатся ULR в очереди

while (true)

{

}

//устанавливаем флажок хендла, что бы сообщить главному потоку о том, что мы отработали

((ManualResetEvent)handle).Set();

}

Здесь главный поток запускает три дочерних потока, которые начинают скачивать сайты. Пока они работают - главный поток ожидает в точке WaitHandle.WaitAll(handles);. Как только все три потока завершат работу - главный поток продолжит свое выполнение.

Чтобы при любой возникающей ошибке в потоках, потоки продолжали работу, но в конце работы программы, ошибки отобразились бы пользователю мы будем использовать блок try{}catch{} в методе который создает потоки - не сможет отловить ошибки самих потоков. Это значит, что отлавливать ошибки потоков нужно внутри того метода, где поток и работает.

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

приложение программа многопоточный

4. ТЕСТИРОВАНИЕ ПРИЛОЖЕНИЯ

Протестировав приложение с разным количеством потоков, был получен такой результат: см. рисунок 2 - рисунок 6.

Рисунок 2 - Работа приложения в однопоточном режиме

Рисунок 3 - Работа приложения в два потока

Рисунок 4 - Работа приложения в три потока

Рисунок 5 - Работа приложения в четыре потока

Рисунок 6 - Работа приложения в пять потоков

Таблица 1 Таблица отношения времени загрузки страниц к количеству потоков

Количество запущенных одновременных потоков

Время в секундах

1

8.792

2

4.856

3

3.422

4

3.263

5

3.191

Рисунок 7 - График зависимости времени на загрузку страниц от количества одновременных потоков

Проанализировав данные таблицы 1 и графика зависимости времени на загрузку страниц от количества потоков см. рисунок 7, можно сделать вывод что, использование многопоточности дает хороший результат при работе с удаленными объектами, такими как web-страницы или базы данных. Если запрос к web-странице или базе данных выполняется достаточно долго, то для этого лучше создать отдельный поток, предоставив пользователю возможность продолжить работу с другими данными.

ЗАКЛЮЧЕНИЕ

В курсовой работе были изучены методы создания многопоточных приложений с использованием языка C# в Visual Studio.NET.

Построены алгоритмы и структура программы многопоточного приложения. Полученное приложение синхронизирует выполнение процессов загрузки web-страниц. Данная программа реализована на языке C#, исполняемый файл занимает 7 680 байт, в приложении приведен полный текст программы.

ЛИТЕРАТУРА

1. Net. Сетевое программирование / А. Мунгале, В. Кумар, К. Нагел, Номан Лагари, Т. Паркер, Ш. Шивакумар, Э. Кровчик. М.: Лори, 2007. 416 с.

2. Райян Б. Основы разработки приложений на платформе Microsoft .NET Framework. Учебный курс Microsoft / Б. Райян, Т. Нортрап, Ш. Вилдермьюс. СПб.: Питер, 2007. 864 с.

3. Байдачный С. .Net Framework 2.0. Секреты создания Windows-приложений / С. Байдачный. М.: Солон, 2006. 520 с.

4. Шеферд Д. Программирование на Microsoft Visual C++.NET. Мастер-класс /Д. Шеферд. М.: Русская Редакция, 2005. 928 с.

5. Материалы сайта antichat.ru

6. Материалы сайта csharpcoding.org

ПРИЛОЖЕНИЕ

Листинг приложения downloader

using System;

using System.Collections.Generic;

using System.Text;

using System.Threading;

using System.Net;

using System.IO;

using System.Diagnostics;

namespace Downloader

{

class Program

{

//очередь адресов для закачки

static Queue<string> URLs = new Queue<string>();

//список скачанных страниц

static List<string> HTMLs = new List<string>();

//локер для очереди адресов

static object URLlocker = new object();

//локер для списка скачанных страниц

static object HTMLlocker = new object();

//очередь ошибок

static Queue<Exception> exceptions = new Queue<Exception>();

static void Main(string[] args)

{

Console.WriteLine("Введите количество потоков для работы");

int s = Convert.ToInt32(Console.ReadLine());

var sw = new Stopwatch();//устанавливаем таймер и запускаем его

sw.Start();

URLs.Enqueue("http://google.com");

URLs.Enqueue("http://yandex.ru");

URLs.Enqueue("http://vorstu.ru");

URLs.Enqueue("http://mail.ru");

URLs.Enqueue("http://ru.akinator.com");

URLs.Enqueue("http://really.ru");

URLs.Enqueue("http://internat.ax3.net");

URLs.Enqueue("http://kaspersky.ru");

//создаем массив хендлеров, для контроля завершения потоков

ManualResetEvent[] handles = new ManualResetEvent[s];

//создаем и запускаем 3 потока

for (int i = 0; i < s; i++)

{

handles[i] = new ManualResetEvent(false);

(new Thread(new ParameterizedThreadStart(Download))).Start(handles[i]);

}

//ожидаем, пока все потоки отработают

WaitHandle.WaitAll(handles);

//проверяем ошибки, если были - выводим

foreach (Exception ex in exceptions)

Console.WriteLine(ex.Message);

//сохраняем закачанные страницы в файлы

try

{

for (int i = 0; i < HTMLs.Count; i++)

File.WriteAllText(i + ".html", HTMLs[i]);

Console.WriteLine(HTMLs.Count + " Файлов сохранено");

}

catch (Exception ex) { Console.WriteLine(ex); }

sw.Stop(); //останавливаем таймер

Console.WriteLine("Загрузка завершена за - "+sw.Elapsed.ToString()+" секунд ");

Console.ReadLine();

}

public static void Download(object handle)

{

//будем крутить цикл, пока не закончатся ULR в очереди

while (true)

try

{

string URL;

//блокируем очередь URL и достаем оттуда один адрес

lock (URLlocker)

{

if (URLs.Count == 0)

break; //адресов больше нет, выходим из метода, завершаем поток

else

URL = URLs.Dequeue();

}

Console.WriteLine(URL + " - старт загрузки ...");

//скачиваем страницу

WebRequest request = WebRequest.Create(URL);

HttpWebResponse response = (HttpWebResponse)request.GetResponse();

string HTML = (new StreamReader(response.GetResponseStream())).ReadToEnd();

//блокируем список скачанных страниц, и заносим туда свою страницу

lock (HTMLlocker)

HTMLs.Add(HTML);

//

Console.WriteLine(URL + " - загружен (" + HTML.Length + " байт)");

}

catch (ThreadAbortException)

{

//это исключение возникает если главный поток хочет завершить приложение

//просто выходим из цикла, и завершаем выполнение

break;

}

catch (Exception ex)

{

//в процессе работы возникло исключение

//заносим ошибку в очередь ошибок, предварительно залочив ее

lock (exceptions)

exceptions.Enqueue(ex);

//берем следующий URL

continue;

}

//устанавливаем флажок хендла, что бы сообщить главному потоку о том, что мы отработали

((ManualResetEvent)handle).Set();

}

}

}

Размещено на Allbest.ru


Подобные документы

Работы в архивах красиво оформлены согласно требованиям ВУЗов и содержат рисунки, диаграммы, формулы и т.д.
PPT, PPTX и PDF-файлы представлены только в архивах.
Рекомендуем скачать работу.