close

Вход

Забыли?

вход по аккаунту

?

547.Языки программирования Ч 2 Якимова О П

код для вставкиСкачать
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Министерство образования и науки Российской Федерации
Ярославский государственный университет им. П. Г. Демидова
Кафедра компьютерной безопасности
и математических методов обработки информации
О. П. Якимова
И. М. Якимов
В. Л. Дольников
Языки программирования
Часть 2
Лабораторный практикум
Рекомендовано
Научно-методическим советом университета для студентов,
обучающихся по специальности Компьютерная безопасность
Ярославль 2012
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
УДК 004.43(076.5)
ББК З973.2-018.1я73
Я 41
Рекомендовано
Редакционно-издательским советом университета
в качестве учебного издания. План 2012 года
Рецензент
кафедра компьютерной безопасности
и математических методов обработки информации
Якимова, О. П. Языки программирования. Ч. 2: лаЯ 41 бораторный практикум / О. П. Якимова, И. М. Якимов,
В. Л. Дольников; Яросл. гос. ун-т им. П. Г. Демидова. –
Ярославль : ЯрГУ, 2012. – 56 с.
Лабораторный практикум предназначен для студентов,
обучающихся по специальности 090301.65 Компьютерная
безопасность (дисциплина «Языки программирования»,
цикл С3), очной формы обучения.
УДК 004.43(076.5)
ББК З973.2-018.1я73
 Ярославский государственный
университет им. П. Г. Демидова,
2012
2
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Лабораторная работа 1
Сериализация
Цель работы: изучение процесса сериализации объектов, посредством которого можно сохранять и получать состояние объекта в любом типе (и из любого типа), производном от
System.IO.Stream.
Необходимые теоретические сведения
1. Понятие сериализации объектов
Сериализация представляет собой процесс преобразования
объекта в поток байтов с целью сохранения его в памяти, в базе
данных или в файле. Ее основное назначение – сохранить состояние объекта для того, чтобы иметь возможность воссоздать его
при необходимости. Обратный процесс называется десериализацией. Применяя эту технологию, очень просто сохранять большие объемы данных (в различных форматах) с минимальными
усилиями. Во многих случаях сохранение данных приложения с
использованием служб сериализации выливается в код меньшего
объема, чем применение классов для чтения/записи из пространства имен System.IO.
Например, предположим, что создано настольное приложение с графическим интерфейсом, в котором необходимо предоставить пользователям возможность сохранения их предпочтений
(цвета окон, размер шрифта и т. п.). Для этого можно определить
класс по имени UserPrefs и инкапсулировать в нем примерно два
десятка полей данных. В случае применения типа
System.IO.BinaryWriter придется вручную сохранять каждое поле
объекта UserPrefs. Аналогично, когда понадобится загрузить данные из файла обратно в память, придется использовать
System.IO.BinaryReader и опять-таки вручную читать каждое значение, чтобы реконструировать новый объект UserPrefs. Сэкономить
значительное время можно, снабдив класс UserPrefs атрибутом
Serializable.
[Serializable] public class UserPrefs 3
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
{ public string FontColor { get; set;} public int FontSize { get; set;} . . . } Тогда можно будет существенно сократить объем кода при
сохранении и/или восстановлении состояния объекта:
UserPrefs user1 = new UserPrefs (); user1.FontColor = "Blue"; user1.FontSize =14; . . . // BinaryFormatter сохраняет данные в двоичном формате. BinaryFormatter binFmt = new BinaryFormatter(); // Сохранение объекта в файле. using(Stream fStream = new FileStream("user.dat", FileMode.Create, FileAccess.Write, FileShare.None)) {
binFmt.Serialize(fStream, user1); }
Итак, для сериализации требуется объект, который будет сериализован, поток, который будет содержать сериализованный
объект, и объект Formatter. Классы, необходимые для сериализации и десериализации объектов, содержатся в пространстве имен
System.Runtime.Serialization.
2. Настройка объектов для сериализации
Рассмотрим детали процесса сериализации. Когда объект сохраняется в потоке, все ассоциированные с ним данные (т. е. данные базового класса и содержащиеся в нем объекты) также автоматически сериализуются. Для корректного сохранения данных
набор взаимосвязанных объектов, участвующих в этом, представляется графом объектов. Например, пусть существует набор
классов, представляющий фигуры на плоскости: класс точки
Point, базовый класс Shape, его наследник класс Circle, – отношения между ними можно представить с помощью графа (рис. 1).
4
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Shape 3
Circle 1
Point 2
Рис. 1. Граф объектов
Среда CLR хранит подобные графы в виде формулы, которая
выглядит примерно так: [Shape 3], [Point 2], [Circle 1, ref 2, ref 3], причем граф объектов строится автоматически. Но, чтобы сделать
объект доступным для служб сериализации .NET, требуется пометить каждый связанный класс (или структуру) атрибутом
[Serializable].
Заметим, что атрибут [Serializable] не может наследоваться от
родительского класса. Поэтому при наследовании типа, помеченного [Serializable], дочерний класс также должен быть помечен
[Serializable], или же его нельзя будет сохранить в потоке.
Если некоторый класс имеет поля, которые не должны (или
не могут) участвовать в сериализации, то можно пометить такие
поля атрибутом [NonSerialized]. Это помогает сократить размер
хранимых данных при условии, что в сериализуемом классе есть
данные, которые не следует «запоминать» (например, фиксированные значения, случайные значения, кратковременные данные
и т. п.).
Приведем пример описания уже упоминавшейся иерархии
классов:
[Serializable] public class Point { public int x, y; [NonSerialized] public string PointDescription = "XOY"; // это поле не сериализуется public Point() { } public Point(int x, int y, string description) { this.x = x; this.y = y; this.PointDescription = description; } } 5
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
[Serializable] public abstract class Shape { protected string petName; public Shape() { petName = "NoName"; } public Shape(string s) { petName = s; } public string PetName { get; set; } } [Serializable] public class Circle : Shape { Point center; public Circle() { } public Circle(string name, int x, int y) : base(name) { center = new Point(x, y, "center of circle"); } . . . } Следующий шаг – это выбор формата для хранения данных.
3. Выбор форматера сериализации
Для сохранения состояния объектов используется один из
трех форматов, которым соответствуют объекты-форматеры, а
именно двоичный формат, SOAP или XML.
При двоичной сериализации используется двоичная кодировка, обеспечивающая компактную сериализацию объекта для хранения или передачи в сетевых потоках на основе сокетов. Кроме
того, когда используется тип BinaryFormatter, он сохраняет не
только данные полей объектов, но также полное квалифицированное имя каждого типа и полное имя определяющей его сборки (имя, версия, маркер общедоступного ключа и культура). Эти
дополнительные элементы данных делают BinaryFormatter идеальным выбором, когда необходимо передавать объекты по значе6
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
нию (т. е. полные копии) между границами машин для использования в .NET-приложениях.
Тип SoapFormatter сохраняет состояние объекта в виде сообщения SOAP (стандартный XML-формат для передачи и приема
сообщений от веб-служб).
При XML-сериализации открытые поля и свойства объекта
или параметры и возвращаемые значения методов сериализуются
в XML-поток в соответствии с особым документом, составленным на языке XSD (язык определения схемы XML). XMLсериализация приводит к образованию строго типизированных
классов с открытыми свойствами и полями, которые преобразуются в формат XML.
Заметим, что форматы XML и SOAP сохраняют только значения открытых полей и/или свойств объекта.
Сериализация объектов с использованием BinaryFormatter
Для сохранения состояния объекта в двоичном формате нужно:
 использовать пространство имен
System.Runtime.Serialization.Formatters.Binary;
 создать файл для хранения данных;
 создать экземпляр BinaryFormatter и передать ему в метод
Serialize файл и сохраняемый объект.
Взгляните на соответствующий фрагмент кода:
Circle mycircle = new Circle("Krug", 2, 2); BinaryFormatter binFmt = new BinaryFormatter(); Stream fStream = new FileStream("Circle.dat", FileMode.Create, FileAccess.Write, FileShare.None); binFmt.Serialize(fStream, mycircle); fStream.Close(); Объекты можно сериализовать в любой класс – наследник
Stream, который представляет собой область памяти, сетевой поток и т. д.
Для восстановления объекта из файла необходимо открыть
соответствующий файл и просто вызвать метод Deserialize класса
7
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
BinaryFormatter. Этот метод возвращает объект типа System.Object,
поэтому необходимо приведение типов:
fStream = File.OpenRead("Circle.dat"); Circle circleFromDisk = (Circle)binFmt.Deserialize(fStream); fStream.Close(); Также легко сохранить и восстановить коллекцию объектов.
Создадим список из трех окружностей:
List<Shape> shapes = new List<Shape> { new Circle("1", 0, 0), new Circle("2", 3, 3), new Circle("3", 5, 5) }; Создадим файл для хранения данных:
Stream shapesFile = new FileStream("data.dat", FileMode.Create, FileAccess.Write, FileShare.None); И вызовем метод Serialize:
binFmt.Serialize(shapesFile, shapes); shapesFile.Close(); Восстановление сохраненной коллекции:
shapesFile = File.OpenRead("data.dat"); List<Shape > sFromDisk = (List<Shape>)binFmt.Deserialize(shapesFile); Сериализация объектов с использованием SoapFormatter
Следующий форматер, которым мы воспользуемся, будет
SoapFormatter (SOAP = Simple Object Access Protocol – простой
протокол доступа к объектам). Для работы с ним нужно установить ссылку на сборку System.Runtime.Serialization.Formatters.Soap.dll и импортировать пространство имен System.Runtime.Serialization.Formatters.Soap.
Для сохранения и извлечения данных об окружности в виде
сообщения SOAP можно просто заменить в предыдущем примере
все вхождения BinaryFormatter на SoapFormatter:
SoapFormatter soapFmt = new SoapFormatter(); Stream fSm = new FileStream("circle.soap", FileMode.Create, FileAccess.Write, FileShare.None); soapFmt.Serialize(fSm, mycircle); fSm.Close(); 8
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Read the Circle from the soap stream. fSm = File.OpenRead("circle.soap"); circleFromDisk = (Circle)soapFmt.Deserialize(fSm); fSm.Close(); Внутри файла circle.soap находятся XML элементы, которые
описывают значения состояния экземпляра класса circle и отношения между объектами в графе:
<SOAP‐ENV:Body> <a1:Circle id="ref‐1" xmlns:a1="http://schemas.microsoft.com/... "> <center href="#ref‐3"/> <petName id="ref‐4">Krug</petName> <Shape_x002B_petName href="#ref‐4"/> </a1:Circle> <a1:Point id="ref‐3" xmlns:a1="http://schemas.microsoft.com/ . . ."> <x>2</x> <y>2</y> </a1:Point> </SOAP‐ENV:Body> Код сериализации контейнера с данными в формате SOAP
аналогичен соответствующему коду для двоичной сериализации.
Сериализация объектов с использованием XmlSerializer
Третий класс форматера XmlSerializer предназначен для сохранения общедоступного состояния заданного объекта в виде чистой XML-разметки, и формат его использования отличается от
работы с типами SoapFormatter или BinaryFormatter. Рассмотрим
следующий код (предполагается, что было импортировано пространство имен System.Xml.Serialization):
XmlSerializer xmlFmt = new XmlSerializer(typeof(Circle), new Type[] { typeof(Shape), typeof(Point)}); Stream fm = new FileStream("circle.xml", FileMode.Create, FileAccess.Write, FileShare.None); xmlFmt.Serialize(fm, mycircle); fm.Close(); 9
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Главное отличие XmlSerializer от других форматеров состоит в
том, что при создании объекта форматера необходимо указать
информацию о всех типах, связанных с классом, чей экземпляр
необходимо сериализовать. В сгенерированном файле XML находятся показанные ниже данные XML:
<Circle xmlns:xsi="http://www.w3.org/2001/XMLSchema‐instance" xmlns:...> <PetName>Krug</PetName> <center> <x>2</x> <y>2</y> <PointDescription>center of circle</PointDescription> </center> </Circle> Кроме того, класс XmlSerializer требует, чтобы все сериализованные типы в графе объектов поддерживали конструктор по
умолчанию. Если этого не сделать, во время выполнения сгенерируется исключение InvalidOperationException.
Теперь рассмотрим сериализацию коллекций объектов с помощью XmlSerializer. Добавим в описанную выше иерархию классов класс Rectangle:
[Serializable] public class Rectangle : Shape { public Point topLeft, downRight; public Rectangle() { } public Rectangle(string name, int x0, int y0, int x1, int y1): base(name) { topLeft = new Point(x0, y0, "topleftvertex"); downRight = new Point(x1, y1, " downrightvertex"); } . . . } Как и прежде, создадим список фигур и файл для хранения
данных:
10
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
List<Shape> shapes = new List<Shape> { new Circle("1", 0, 0), new Rectangle("2", 4, 4, 6, 2), new Circle("3", 5, 5) }; Stream shapesFile = new FileStream("data.xml", FileMode.Create, FileAccess.Write, FileShare.None); При создании объекта-форматера, указываются типы, которые связаны с сериализуемым контейнером:
XmlSerializer xmlFmt = new XmlSerializer(typeof(List<Shape>), new Type[] { typeof(Rectangle), typeof(Point), typeof(Circle) }); xmlFmt.Serialize(shapesFile,shapes); shapesFile.Close(); Следующий фрагмент кода демонстрирует извлечение данных о фигурах из файла:
shapes.Clear(); shapesFile = File.OpenRead("data.xml"); shapes = (List<Shape>)xmlFmt.Deserialize(shapesFile); foreach (Shape c in shapes) { Console.WriteLine("{0} {1} ", c.PetName, c.ToString()); } shapesFile.Close(); 4. Поддержка версий объектов
Вернемся к примеру, рассмотренному в начале этой главы.
Класс UserPrefs предназначен для сохранения параметров приложения конкретного пользователя. Допустим, возникла необходимость расширить этот класс еще одним полем, которое задает начальный размер окна. Новое поле появилось, но в файле сохранены значения настроек пользователя без него и при восстановлении значений возникнет исключение. Выход состоит в использовании атрибута [OptionalField], который устанавливается перед новым полем. В этом случае восстановление состояния объекта
происходит без проблем, а новое поле получает значение по
умолчанию.
11
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
5. Настройка сериализации с использованием атрибутов
Чаще всего схема сериализации по умолчанию, предоставляемая платформой .NET, – это то, что нужно. Пометьте атрибутом [Serializable] класс и передайте его объект выбранному форматеру для обработки. В некоторых случаях может понадобиться
вмешаться в процесс сериализации. Например, вы хотите добавить дополнительные данные в поток, которые напрямую не отображаются на поля сохраняемого объекта (временные метки,
уникальные идентификаторы и т. п.). Тогда нужно определить
методы, помеченные одним из следующих атрибутов:
[OnDeserialized] [OnDeserializing] [OnSerialized] [OnSerializing] этим атрибутом помечается метод, который будет вызван немедленно после десериализации объекта
этим атрибутом помечается метод, который будет вызван перед процессом десериализации
этим атрибутом помечается метод, который будет вызван немедленно после того, как объект сериализован
этим атрибутом помечается метод, который будет вызван перед процессом сериализации
В случае применения этих атрибутов метод должен быть определен так, чтобы принимать параметр StreamingContext и нe
возвращать ничего (иначе сгенерируется исключение времени
выполнения). Обратите внимание, что применять каждый из атрибутов сериализации не обязательно, можно просто вмешаться в
те стадии процесса сериализации, которые интересуют. Для иллюстрации ниже приведен новый класс Point, описание объекта
которого будет сохраняться в ином формате:
[Serializable] public class Point { public int x, y; public string PointDescription; public Point() { } public Point(int x, int y, string description) { this.x = x; this.y = y; this.PointDescription = description; 12
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
} [OnSerializing] public void OnSerializingMethod(StreamingContext cntx) { this.PointDescription = string.Format(" {0} x={1} y={2}", this.PointDescription, this.x, this.y); } . . . } 6. Задание для лабораторной работы 1
Напишите приложение, которое демонстрирует применение
сериализации. В этом приложении:
1) создайте пять экземпляров класса из лабораторной работы
1-го семестра и поместите их в коллекцию;
2) выведите на экран меню, позволяющее выбрать тип сериализации: двоичный, XML, SOAP;
3) согласно выбору пользователя, сохраните созданную коллекцию в файл;
4) восстановите сохраненную информацию с диска и продемонстрируйте, что она не изменилась.
Лабораторная работа 2
Многопоточные приложения
Цель работы: изучение различных подходов к созданию
многопоточных приложений, их преимущества и недостатки. Использование библиотек ввода-вывода.
Необходимые теоретические сведения
1. Задачи многопоточности
Многопоточность представляет собой возможность выполнять несколько кусков кода «параллельно». Слово «параллельно»
заключено в кавычки потому, что на самом деле процессор ком13
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
пьютера способен одновременно выполнять только одну инструкцию. Поэтому в каждый момент времени на нем выполняется
только один поток. Соответственно, двухъядерный процессор
способен одновременно выполнять два потока и т. д.
Каким же образом тогда реализуется многопоточность? Операционная система поддерживает список активных потоков. В
очередной момент времени она выбирает из него один поток и
передает ему управление на время, называемое квантом времени.
В течение этого времени код данного потока выполняется процессором. Затем операционная система принудительно останавливает этот поток и сохраняет содержимое регистров процессора
и другую необходимую потоку для нормальной работы информацию в области памяти, называемой контекстом потока. Затем
операционная система выбирает из списка потоков другой активный поток, загружает его контекст потока и передает этому потоку управление на следующий квант времени. Такая техника, когда операционная система сама следит за распределением времени между потоками, прерывая их в случае необходимости, называется вытесняющей многозадачностью.
Какой же вывод можно сделать из этой системы реализации
многозадачности? Операционная система тратит время на остановку потока, сохранение его контекста, выбор нового потока и
загрузку его контекста. Чем больше в системе потоков, тем
больше накладные расходы на эти операции. Поэтому несколько
потоков всегда выполняются медленнее, чем один поток.
Многопоточность не ускоряет работу, она сокращает время
отклика системы. Если вы хотите выполнить какую-то длительную операцию, то выполнение ее в том же потоке, в котором происходит взаимодействие с пользователем (потоке пользовательского интерфейса) приведет к «зависанию» системы. То есть до
тех пор, пока задача не будет выполнена, система не будет реагировать на действия пользователя. Если же вы выполняете данную
длительную задачу в отдельном потоке, то за счет того, что операционная система время от времени передает управление потоку
пользовательского интерфейса, он имеет возможность обрабатывать действия пользователя. Особенно это важно при работе с
14
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
оконным интерфейсом Windows. Если загрузить поток интерфейса
какой-либо задачей, то окна перестанут отрисовываться.
Следующие требования обычно предъявляются к механизмам, обеспечивающим поддержку многопоточности в языке программирования:
1) простые средства создания отдельных потоков кода;
2) нам необходимо знать, когда поток завершил свою работу.
Также необходимы механизмы, позволяющие подождать, пока
поток не завершит свое выполнение;
3) иногда требуется отменить выполнение задачи, которую
исполняет поток;
4) иногда необходимо приостановить исполнение потока, например, чтобы освободить используемые им вычислительные ресурсы;
5) необходимы механизмы, позволяющие синхронизировать
доступ нескольких потоков к различным ресурсам.
Далее мы рассмотрим, как реализуются эти и другие связанные с потоками задачи на платформе .NET.
2. Методы создания потоков
Рассмотрим основные методы создания потоков в .NET
Framework.
2.1. Делегаты
Наверное, наиболее простым методом выполнения какоголибо метода в отдельном потоке является использование делегата. Пусть у нас есть метод, выполняющий какую-либо длительную задачу:
private void CheckPrimeNumber(long checkNumber){…} Для того чтобы выполнить его в отдельном потоке, необходимо создать делегат, соответствующий сигнатуре данного метода:
private delegate void CheckPrimeNumberDelegate(long checkNumber); Затем создайте объект делегата, передав его конструктору
метод, который вы хотите выполнить в отдельном потоке:
CheckPrimeNumberDelegate dlgt = new CheckPrimeNumberDelegate(CheckPrimeNumber); 15
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Запуск выполнения метода в новом потоке осуществляется
вызовом метода BeginInvoke делегата.
dlgt.BeginInvoke(1000000021, null, null); Этому методу передаются те же параметры, что передавались
бы методу CheckPrimeNumber, а также некоторые другие. Вот и все.
Ваш метод будет выполнен в отдельном потоке, не останавливая
выполнение основного потока.
Ожидание завершения работы потока
Рассмотренный нами случай был наиболее простым. Однако
предположим, что наш метод, который должен выполняться в отдельном потоке, возвращает некоторый результат:
private static long GetDivider(long checkNumber){…} Очевидно, нам необходимо знать две вещи: 1) когда поток
завершил работу? 2) как получить результат, возвращенный
функцией, выполнявшейся в этом потоке?
Рассмотрим решение первой задачи. Как и в предыдущем
случае, необходимо создать делегат, соответствующий нашему
методу, создать его экземпляр и вызвать метод BeginInvoke.
private delegate long GetDividerDelegate(long checkNumber); GetDividerDelegate dlgt = new GetDividerDelegate(GetDivider); Однако теперь нас интересует объект, возвращаемый методом BeginInvoke:
IAsyncResult res = dlgt.BeginInvoke(1000000021, null, null); Первый способ дождаться завершения созданного нами потока – проверять свойство IsCompleted объекта IAsyncResult:
while (!res.IsCompleted) { continue; } Внутри данного цикла вы можете производить какую-либо
работу в основном потоке.
Второй подход заключается в использовании объекта, возвращаемого свойством AsyncWaitHandle объекта IAsyncResult:
res.AsyncWaitHandle.WaitOne(); 16
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Более подробно об объектах WaitHandle мы будем говорить в
разделе, посвященном синхронизации потоков. Сейчас же важно
то, что вызов метода WaitOne этого объекта позволяет нам дождаться завершения нашего потока. Однако знайте, что вызов этого метода блокирует вызвавший его поток до тех пор, пока не будет завершен ваш поток.
В связи с этим, наверное, самым удобным способом получения уведомления о том, что ваш поток завершился, является делегат обратного вызова. Этот делегат передается одним из параметров метода BeginInvoke:
dlgt.BeginInvoke(1000000021, new AsyncCallback(GetDividerFinished), null); Сигнатура этого метода:
private static void GetDividerFinished(IAsyncResult res){…} Как видно, ему передается такой же объект IAsyncResult, что
возвращался при вызове метода BeginInvoke. При использовании
такого подхода основной поток не будет заблокирован, а в момент завершения вашего потока будет вызван метод
GetDividerFinished.
Получение результата работы метода,
выполнявшегося в отдельном потоке
Теперь, когда мы дождались завершения работы нашего потока, пришло время получить результат его работы. Для этого
служит метод EndInvoke делегата, использовавшегося для вызова
BeginInvoke. Метод EndInvoke возвращает тот же объект, что вернул
и выполнявшийся в отдельном потоке метод. Учтите, что эти методы должны вызываться у одного и того же экземпляра делегата. Методу EndInvoke передается объект IAsyncResult, возвращенный
методом BeginInvoke:
Console.WriteLine("Divider of {0} is {1}.", 1000000021, dlgt.EndInvoke(res)); Учтите также, что вызов EndInvoke до завершения работы
потока приведет к блокировке вызвавшего его потока до тех пор,
пока работа метода, выполняющегося в отдельном потоке, не будет завершена. Поэтому обычно EndInvoke вызывают, только дождавшись его завершения.
Если вы использовали IsCompleted или AsyncWaitHandle для
ожидания окончания работы потока, то все в порядке. У вас есть
17
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
и делегат, и IAsyncResult. Но при использовании делегата обратного вызова есть проблема. Ему передается IAsyncResult в качестве
параметра, но делегата, инициировавшего поток, у него нет. Для
того чтобы получить его, используется обходной маневр:
AsyncResult aRes = res as AsyncResult; GetDividerDelegate dlgt = aRes.AsyncDelegate as GetDividerDelegate; Теперь можно спокойно вызывать EndInvoke. Если же такой
искусственный подход вам не нравится, есть еще один способ.
Методу BeginInvoke в последнем параметре вы можете передать
любой объект. В последствии этот объект будет доступен через
свойство AsyncState объекта IAsyncResult. Вы можете передать в качестве этого параметра сам делегат:
dlgt.BeginInvoke(1000000021, new AsyncCallback(GetDividerFinished), dlgt); а затем в делегате обратного вызова извлечь его:
GetDividerDelegate dlgt = res.AsyncState as GetDividerDelegate; Теперь вы так же можете вызывать EndInvoke.
Таким образом, создание потоков с помощью делегатов имеет следующие достоинства и недостатки:
1) легко создать поток на основе любого метода;
2) легко дождаться завершения работы потока как синхронно
(с блокированием ожидающего потока), так и асинхронно (без
блокирования);
3) можно получить результат работы метода, вызываемого в
отдельном потоке;
4) нельзя стандартными способами прервать работу потока
или приостановить его, чтобы освободить вычислительные ресурсы. Вам придется писать для этого собственный код.
Полезный совет
Неудобно создавать для каждого вашего метода отдельный
делегат. В состав .NET Framework входят два обобщенных делегата – Action и Func. Первый из них позволяет описать метод, не
возвращающий параметров, второй – метод, возвращающий результат.
18
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
2.2. Класс Thread
Вторым способом создания отдельного потока является использование класса Thread. Необходимо создать объект класса Thread:
Thread trd = new Thread(new ThreadStart(CheckPrimeNumber)); Конструктору этого объекта передается экземпляр делегата
ThreadStart. Он описывает метод без параметров, не возвращающий значения. Для запуска потока на выполнение нужно вызвать
метод Start объекта Thread:
trd.Start(); Отсутствие возможности передать методу, который будет
выполняться в отдельном потоке, каких-либо параметров, несомненно, является ограничением. Поэтому существует несколько
иной синтаксис создания потока, который позволяет передать
вашему методу параметры. При этом в конструктор класса Thread
передается не делегат ThreadStart, а делегат ParameterizedThreadStart.
Он описывает метод с одним параметром типа object:
Thread trd = new Thread(new ParameterizedThreadStart(CheckParameterPrimeNumber)); Теперь методу Start передается параметр, который будет передан вашему методу при выполнении его в отдельном потоке:
trd.Start(1000000021); Использование параметра типа object очень удобно, т. к. позволяет передавать в ваш метод объект любого типа. В данном
случае мы передаем число. Внутри вашего метода вы просто
должны выполнить приведение параметра к правильному типу:
private static void CheckParameterPrimeNumber(object state) { long checkNumber = Convert.ToInt64(state); … } Таким образом вы можете передавать в ваш метод и несколько параметров, упаковав их в отдельный объект.
Недостатком такого подхода является отсутствие строгой типизации. Ничто не помешает мне вызвать метод Start с параметром типа string. Компилятор пропустит это, не выдав сообщения
19
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
об ошибке. Тем не менее во время исполнения ошибка произойдет, поскольку string не может быть приведен к long. Стандартным
методом решения этой проблемы является использование классаобертки. Вы выносите метод, который вы хотите запустить в отдельном потоке, и параметры, необходимые для его работы, в отдельный класс:
class PrimeChecker { long checkNumber; public PrimeChecker(long checkNumber) { this.checkNumber = checkNumber; } public void Check() { for (long d = 2; d < checkNumber / 2; d++) { if (checkNumber % d == 0) { Console.WriteLine("{0} is not prime. Divider is {1}.", checkNumber, d); return; } } Console.WriteLine("{0} is prime.", checkNumber); } } Создание вашего потока теперь выглядит следующим образом:
PrimeChecker pc = new PrimeChecker(1000000021); Thread trd = new Thread(new ThreadStart(pc.Check)); trd.Start(); Этот прием позволяет сохранить строгую типизацию.
Ожидание завершения потока
Класс Thread предоставляет только синхронные методы ожидания завершения потока. Первый из них – свойство IsAlive:
20
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
while (trd.IsAlive) { continue; }
Внутри данного цикла вы можете делать какую-либо работу.
Другой подход – использование метода Join.
trd.Join(); Вызов этого метода блокирует вызвавший его поток до тех
пор, пока поток trd не закончит свою работу.
Недостатком этого метода, несомненно, является блокирование основного потока. К сожалению, класс Thread не предоставляет возможности узнать о завершении работы потока с помощью
делегата обратного вызова.
Управление выполнением потока
Класс Thread предоставляет возможности приостановить выполнение потока, снова запустить его и совсем прервать поток.
Рассмотрим их. Приостановка и продолжение выполнения потока
осуществляются вызовом методов Suspend и Resume. Следует отметить, что Microsoft считает эти методы устаревшими и не рекомендует их использовать. Поэтому вам не следует прибегать к
их услугам без крайней необходимости. Данные методы вызывают исключение в случае, если поток не запущен или уже завершен. Также метод Resume вызывает исключение в случае, если
поток не приостановлен методом Suspend.
Более сложно обстоит дело с прерыванием работы потока.
В реальных приложениях поток выполняет некоторую работу, т. е.
он изменяет данные. Если прервать его неожиданно, то данные могут оказаться в нерабочем состоянии. Поэтому .NET реализует механизм, уведомляющий поток о том, что его хотят досрочно завершить. Дело программиста – завершить поток правильно.
Первый метод, который прерывает поток, – Abort.
trd.Abort(); Вызов этого метода приводит к генерации в соответствующем потоке исключения ThreadAbortException. Задача программиста – обработать его и правильно завершить поток:
21
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
private static void CheckPrimeNumberWithAbort() { try { … } catch (ThreadAbortException) { … Console.WriteLine("Thread is aborted"); } }
Особенностью, о которой следует знать, является то, что вызов метода Abort, когда поток находится в приостановленном состоянии, приводит к появлению исключения. Поэтому вам придется вызвать Resume перед использованием Abort.
Вторым методом, который прерывает работу потока, является метод Interrupt. Отличие его от Abort заключается в том, что он
прерывает работу не любого потока, а только того, который находится в состоянии ожидания (например, ожидает завершения
работы другого потока, вызвав его метод Join) или «спит». Последний термин будет подробнее рассмотрен ниже. Если же поток не находится в таком состоянии, метод Interrupt ничего не
сделает. Таким образом, Interrupt прерывает скорее «сон» или
ожидание потока, чем его самого. При этом в самом потоке возникает исключение ThreadInterruptedException, которое вы можете
обработать.
«Сон» потока
Настало время рассмотреть, что же такое «сон» потока. Статический метод Sleep класса Thread заставляет вызвавший его поток «заснуть», т. е. то время, которое указано в параметре метода,
поток не будет работать, ему не будут передаваться кванты времени. Как же это реализуется? Как уже говорилось выше, операционная система поддерживает список активных потоков. Но,
кроме него, существует еще и список спящих потоков. Вызов метода Sleep помещает поток в этот список. Потоки в этом списке
не участвуют в распределении квантов времени. Они ждут за22
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
вершения времени своего сна. После этого поток снова перемещается в список активных потоков.
Вы можете вызывать метод Sleep для ожидания какого-либо
события, хотя существуют методы лучше. О них мы будем говорить далее.
Единственным исключением из описанного механизма работы метода Sleep является его вызов с параметром 0. В этом случае
поток не перемещается в список спящих потоков. Просто его выполнение немедленно прерывается, и его квант времени передается другому потоку. Но поток по-прежнему остается в списке
активных потоков и участвует в распределении времени.
Приоритет потоков
Класс Thread имеет экземплярное свойство Priority, задающее
приоритет потока. Его значением является член перечисления
ThreadPriority. Названия членов этого перечисления говорят сами за
себя. Значение Normal устанавливается по умолчанию. Вы можете
сделать приоритет потока больше или меньше. Но что же такое
этот приоритет? Может показаться, что потоки с большим приоритетом просто чаще получают кванты времени для своего выполнения. Но на самом деле это не так. На самом деле операционная система всегда выбирает из списка активных потоков поток с
наибольшим приоритетом. Поэтому, пока в списке активных потоков есть потоки, приоритет которых выше других, будут выполняться только они. Передать выполнение потокам с более низким
приоритетом можно, вызвав метод Sleep в потоках с высоким
приоритетом. Как уже говорилось, в этом случае данные потоки
будут перемещены из списка активных потоков в список спящих
потоков и не будут участвовать в распределении квантов времени.
Фоновые потоки и потоки «переднего плана»
У класса Thread есть еще одно важное свойство –IsBackground.
По умолчанию оно имеет значение false. Данное свойство определяет, является ли поток фоновым (background) или потоком «переднего плана». Как уже сказано, по умолчанию поток является
потоком «переднего плана». Что же это такое? В системе
Windows существует понятие процесса. Процесс обладает своей
изолированной от других процессов памятью. Таким образом,
разделение на процессы повышает стабильность работы системы,
23
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
поскольку один процесс не может изменять память другого процесса. Каждый процесс при старте обладает одним потоком. Этот
поток называется основным. Однако этот основной поток может
порождать другие (вторичные) потоки. Рассмотрим, что произойдет, если основной поток уже завершил свою работу, но есть выполняющийся вторичный поток. Если вторичный поток является
потоком «переднего плана», то процесс не будет завершен до тех
пор, пока не завершит работу этот вторичный поток. Если же
вторичный поток является фоновым, то его работа будет остановлена и процесс будет завершен. Таким образом, фоновые потоки могут не завершить свою работу при закрытии приложения.
Поэтому нельзя выносить в фоновые потоки обработку критических данных.
Пришло время подвести итоги по использованию класса
Thread. Он имеет следующие достоинства и недостатки:
1) легко создавать отдельные потоки;
2) существуют легкие в использовании механизмы, позволяющие управлять выполнением потока: приостанавливать его,
запускать снова, прерывать поток;
3) легко проводить тонкую настройку потока: задавать приоритет, определять, является ли поток фоновым;
4) немного затруднен механизм передачи параметров в метод
потока;
5) существуют только синхронные способы дождаться завершения потока.
2.3. Класс ThreadPool
Как уже говорилось ранее, если вы создадите слишком много
потоков, то операционная система будет тратить слишком много
времени на переключение между ними. С другой стороны, также
уменьшится число квантов времени, выделяемых данному конкретному потоку за определенный интервал времени (например,
за минуту). Все это может привести к резкому падению производительности не только приложения, которое использует множество потоков, но и всех других приложений на этом компьютере.
Для частичного решения этой проблемы .NET предлагает класс
ThreadPool.
24
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Класс ThreadPool имеет ряд возможностей. Он может производить
ожидание каких-либо событий от вашего лица, но здесь мы рассмотрим только создание нового потока. Это делается очень просто:
ThreadPool.QueueUserWorkItem(new WaitCallback(CheckPrimeNumber), 1000000021); Методу QueueUserWorkItem передается делегат WaitCallback
описывающий не возвращающий значений метод с одним параметром типа object. Второй параметр метода QueueUserWorkItem –
именно то, что будет передано методу делегата WaitCallback. Мы
уже рассматривали использование этого параметра для передачи
различных данных в метод, выполняемый в отдельном потоке.
Все те же методики действенны и здесь.
За счет чего же класс ThreadPool обеспечивает нам выигрыш в
производительности? Этот класс создает сразу же несколько потоков (по умолчанию 25 на каждый процессор компьютера). Но
это пока неактивные потоки, они не выполняют никакой код, под
них просто выделены ресурсы. Когда вы вызываете метод
QueueUserWorkItem, ThreadPool выбирает очередной неактивный поток и выполняет в нем ваш метод. По завершении работы вашего
метода поток возвращается в пул неактивных потоков. Таким образом происходит переиспользование потоков. Их не приходится
создавать и уничножать каждый раз. Если же вы поставили в
очередь слишком много методов на выполнение в отдельных потоках и в пуле не осталось свободных неактивных потоков, то
ThreadPool создаст новые потоки для вас. Но новый поток создается не чаще, чем раз в полсекунды. Это гарантирует, что не будет
предпринята попытка создать сразу много потоков. Кроме того,
за очередные полсекунды какой-либо из выполняемых методов
может закончить свою работу и освободить поток. Тогда создание нового потока не потребуется. В общем, Microsoft рекомендует использовать ThreadPool вместо Thread, когда это только возможно. Большинство технологий Microsoft, созданных в рамках
.NET, используют именно его.
Подведем итоги. Класс ThreadPool имеет следующие достоинства и недостатки:
1) наверно, простейший способ создать отдельный поток;
2) оптимизирован для работы со множеством потоков;
25
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
3) практически никаких механизмов управления или ожидания завершения работы.
2.4. Класс Task
С появлением .NET Framework 4.0 Microsoft предоставляет
нам новые возможности по созданию и управлению многопоточными приложениями. Ряд новых классов для работы с потоками
был введен в пространстве имен System.Threading.Tasks.
Создание потоков
Начнем рассмотрение этого пространства имен с класса Task.
Наиболее простым способом создания потока с использованием
этого класса является вызов метода StartNew у статического свойства Factory:
Task.Factory.StartNew(CheckPrimeNumber); Свойство Factory возвращает объект класса TaskFactory. Этот
объект отвечает за создание объектов типа Task.
У метода StartNew есть много перезагрузок, позволяющих
создавать потоки, которые возвращают результаты, потоки, которым передаются параметры, и т. д:
Task.Factory.StartNew(CheckParameterPrimeNumber, 1000000021); Ожидание завершения потока
Методы класса TaskFactory, в том числе и StartNew, возвращают
объект класса Task. Этот объект предоставляет информацию о потоке. В числе прочих в него входит свойство IsComplete, с помощью которого можно проверить, завершился ли поток:
Task tk = Task.Factory.StartNew(CheckParameterPrimeNumber, 1000000021); while (!tk.IsCompleted) { continue; } Аналогичного результата можно достичь, вызвав метод Wait:
task.Wait(); Как обычно, при этом выполнение вызывающего потока приостанавливается до завершения выполнения потока задачи.
Однако данный класс предоставляет и возможность асинхронно дождаться завершения выполнения потока. Эта функцио26
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
нальность аналогична той, что используют делегаты. Для этого
используется метод FromAsync класса TaskFactory:
Action dlgt = CheckPrimeNumber; Task.Factory.FromAsync(dlgt.BeginInvoke, GetDividerFinished, null); Синтаксис этого метода несколько запутан, но все же позволяет осуществить задуманное. После завершения потока будет вызван указанный метод GetDividerFinished. Такой сложный синтаксис
обусловлен тем, что данный подход обычно используется для вызова методов асинхронной обработки. Их примерами могут служить методы асинхронного чтения/записи у потоков, методы асинхронной работы с Web-сервисами и ряд других. Обычно такие методы представлены парами BeginXXX/EndXXX. Например, следующий код иллюстрирует, как асинхронно прочитать файл:
FileInfo fi = new FileInfo("TextFile1.txt"); byte[] data = new byte[fi.Length]; FileStream fs = File.OpenRead("TextFile1.txt"); Task<int> task = Task<int>.Factory.FromAsync(fs.BeginRead, fs.EndRead, data, 0, data.Length, null); Однако класс Task предоставляет более простой и мощный
способ асинхронно дождаться завершения выполнения потока –
присоединение задач. Этот механизм позволяет начать выполение потока после того, как один или несколько потоков завершат
свое выполнение. Работает этот механизм с помощью метода
ContinueWith:
Task task = new Task(CheckPrimeNumber); task.ContinueWith(GetDividerFinished); task.Start(); Этому методу передается делегат, ссылающийся на метод,
принимающий экземпляр Task в качестве параметра:
private static void GetDividerFinished(Task finished); Этот метод будет выполнен после того, как закончит свою
работу задача. Кроме того, класс TaskFactory имеет методы
ContinueWhenAll и ContinueWhenAny, позволяющие выполнить ваш
метод после того, как завершатся все задачи из списка, или после
того, как завершится любая задача из списка.
27
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Такое выстраивание асинхронных методов в цепочку является в ряде случаев весьма полезным. Рассмотрим следующий пример: пусть наше приложение принимает некоторые данные от
Web-сервиса в Интернет. Получение данных при этом должно
выполняться асинхронно. Кроме того, для сокращения объема
передаваемых данных они сжаты, поэтому после получения данные должны быть разжаты. Операция разжатия данных также
должна выполняться асинхронно. Для выполнения данных действий раньше приходилось создавать сложные цепочки делегатов,
вызывать методы асинхронного исполнения один из другого. Теперь эта задача решается проще:
private static byte[] CompressedData; private static string UncompressedResult; public static void Run() { Task task = new Task(GetCompressedDataFromWebService); task.ContinueWith(UncompressData).ContinueWith(UseUncompressedData); task.Start(); } private static void GetCompressedDataFromWebService() { // code to get data from Web service. . . . Console.WriteLine("Data were obtained from Web service."); } private static void UncompressData(Task finished) { // code to decompress obtained data. . . . Console.WriteLine("Data were uncompressed."); } private static void UseUncompressedData(Task finished) { Console.WriteLine("Data from service is '{0}'.", UncompressedResult); } 28
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Как видите, код вполне прямолинейный. На самом деле, отдельные методы в цепочке могут ничего не знать друг о друге.
Это несомненное достоинство и упрощение. Метод ContinueWith
сам возвращает экземпляр класса Task, что позволяет строить цепочки произвольной длины.
Получение результатов выполнения потока
Класс Task позволяет получить результаты выполнения потока. Для этого используется generic-версия этого класса Task<T>.
Пусть у нас есть метод, возвращающий некоторое значение:
private static long GetDivider(object state) И мы хотим исполнить его асинхронно и получить результат.
Для этого создается экземпляр класса Task<long>:
Task<long> task = Task.Factory.StartNew<long>(GetDivider, 1000000021); Затем необходимо любым способом дождаться завершения
выполнения задачи и получить результат через свойство Result:
Console.WriteLine("Divider of {0} is {1}.", 1000000021, task.Result); Прерывание потока
В .NET 4 появился новый мощный механизм отмены асинхронных или долгих синхронных операций. Он основан на использовании объекта, называемого токеном отмены (cancellation
token). Рассмотрим простейший пример его использования.
Получить экземпляр объекта токена отмены можно с помощью свойства Token класса CancellationTokenSource:
CancellationTokenSource ctSource = new CancellationTokenSource(); CancellationToken token = ctSource.Token; Теперь нужно передать созданный токен в тот метод, который будет выполняться в отдельном потоке, чтобы он мог узнать,
когда его попросят прерваться. Так же нужно передать токен
конструктору объекта Task:
var task = Task.Factory.StartNew<long>(() => GetDivider(1000000021, token), token); Обратите пристальное внимание на этот код. Кроме всего
прочего, он демонстрирует, как с помощью лямбда-выражений
29
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
или анонимных делегатов заставить выполняться в отдельном
потоке метод с любым количеством параметров.
Послать уведомление о том, что потоку нужно прерваться,
очень просто. Для этого нужно вызвать метод Cancel объекта
CancellationTokenSource:
ctSource.Cancel(); В этом случае все токены отмены, созданные с помощью
свойства Token этого объекта, узнают, что требуется отменить
выполнение потока. Однако то, какие действия должен при этом
выполнять поток, является выбором программиста. Внутри вашего метода вы можете узнать, была ли запрошена отмена, с помощью свойства IsCancellationRequested объекта CancellationToken:
if (cToken.IsCancellationRequested) { // some cleaning code. cToken.ThrowIfCancellationRequested(); } Этот код является стандартным кодом прерывания потока.
Вы проверяете, была ли запрошена отмена выполнения, затем
выполняете некоторый код очистки, а потом вызываете метод
ThrowIfCancellationRequested. Последний генерирует OperationCan‐
celedException только в том случае, если была запрошена отмена
выполнения.
Что же происходит, когда генерируется это исключение?
Объект OperationCanceledException несет в себе информацию, какой
именно токен отмены создал его. Если этот токен не совпадает с
тем, что был передан конструктору Task, то данное исключение
расценивается как обычное сообщение об ошибке, поток прерывается и свойство Status объекта Task устанавливается в Faulted.
Если же токены совпадают, то значение свойства Status делается
равным Canceled. При этом если кто-либо ожидает завершения
данного потока (например, с помощью метода Wait), то он получит исключение AggregateException. Это новый тип исключения,
который содержит информацию о нескольких исключениях. Это
необходимо для того, чтобы отлавливать исключения, произо30
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
шедшие сразу в нескольких потоках. Все произошедшие исключения доступны через свойство InnerExceptions:
try { task.Wait(); } catch (AggregateException aex) { foreach(var ex in aex.InnerExceptions ) { Console.WriteLine(ex); } } С помощью токенов отмены вы можете прерывать выполнение нескольких потоков и цепочек потоков, передавая им объект
токена и используя описанные здесь приемы. Класс
CancellationToken также может быть полезен для отмены асинхронных операций.
Подведем итоги. Класс Task имеет следующие достоинства и
недостатки:
1) простой способ создания отдельных потоков;
2) существуют синхронные и асинхронные механизмы ожидания завершения потока;
3) можно отменить выполнение потока;
4) легко составлять цепочки из потоков;
5) легко получить результат выполнения метода потока;
6) немного затруднен механизм передачи типизированных
параметров в поток;
7) невозможно приостанавливать поток и запускать его снова.
2.5. PLINQ
.NET Framework 4.0 принес с собой еще одну возможность,
связанную с многопоточностью. Она связана с обработкой коллекций. Бывают случаи, когда нам нужно обработать в отдельных потоках элементы какой-либо коллекции. Раньше нам пришлось бы
создавать цикл, в котором вызывать новый поток для каждого из
31
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
элементов. Теперь .NET Framework предлагает упрощенный синтаксис.
Класс Parallel
Прежде всего, в .NET Framework 4.0 появился класс Parallel,
который имеет методы For, ForEach и Invoke. Как несложно понять,
первые два из них занимаются тем, что исполняют некоторый метод в цикле. Первому из них передаются границы цикла, а второму – коллекция:
Parallel.For((long)(1000000021 ‐ 20), (long)(1000000021 + 20), CheckParameterPrimeNumber); От обычных циклов for и foreach эти методы отличаются
только тем, что выполняют тело цикла в отдельном потоке для
каждой из итераций цикла.
Следует также отметить, что, хотя каждая итерация выполняется в отдельном потоке, вызов этих методов не вернет управление, пока все итерации не завершатся. Это логично, т. к. нет никаких иных механизмов ожидания завершения всех потоков.
Таким образом, данные методы просто позволяют вам лучше
использовать ресурсы системы (например, несколько процессоров).
Метод Invoke в качестве параметра принимает коллекцию делегатов, каждый из которых он выполнит в отдельном потоке:
Parallel.Invoke( () => CheckParameterPrimeNumber(1000000020), () => CheckParameterPrimeNumber(1000000021), () => CheckParameterPrimeNumber(1000000022), () => CheckParameterPrimeNumber(1000000023) ); Этот метод также вернет управление только после завершения выполнения всех своих делегатов.
Метод AsParallel()
Кроме указанного класса Parallel, Microsoft добавил похожую
поддержку многопоточности в LINQ. Осуществляется она с помощью метода AsPartallel (класс Exercise3). Предположим, что нам
нужно найти все простые числа из некоторого списка. Ранее мы
могли бы сделать это так:
32
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
var primes = from v in values where IsPrime(v) select v; Теперь, когда мы захотели бы перечислить все простые числа
в цикле foreach, наш метод IsPrime последовательно вызывался бы
для каждого из чисел списка values.
Теперь же мы можем заставить систему параллельно проверять несколько чисел сразу:
var primes = from v in values.AsParallel() where IsPrime(v) select v; Все, что для этого нужно, добавить вызов метода AsParallel.
Как видите, добавление многопоточности к обработке коллекций – довольно простое дело. Однако, конечно же, при этом необходимо, чтобы элементы коллекции действительно могли обрабатываться независимо друг от друга. Задача программиста –
убедиться в этом.
3. Синхронизация потоков
В реальном приложении потоки часто используют совместные ресурсы. Это означает, в частности, что несколько потоков
сразу могут обращаться и изменять одну и ту же переменную. Зачастую это может приводить к нежелательным последствиям.
Рассмотрим следующий пример. Есть 3 потока. Первый из них
добавляет элементы в некоторый список. Остальные 2 потока
удаляют элементы из этого списка. Чтобы удаление произошло
правильно, необходимо, чтобы в списке был хотя бы один элемент. Поэтому удаляющие потоки используют следующий код:
if (list.Count > 0) list.RemoveAt(0); Пусть в некоторый момент времени в списке находится
единственный элемент. Вполне возможен следующий вариант
работы удаляющих потоков:
Квант
Удаляющий
Результат
Удаляющий
Результат
времени
поток 1
работы потока 1
поток 2
работы потока 2
1
if (list.Count > 0)
true
2
if (list.Count > 0)
true
3
list.RemoveAt(0);
Ok
4
list.RemoveAt(0);
Exception
33
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Поэтому существует необходимость синхронизировать (упорядочивать) работу потоков, использующих одни и те же данные.
Здесь мы рассмотрим механизмы, существующие для этого в
.NET Framework (проект Synchronization).
3.1. Оператор lock
Наиболее простым способом синхронизации работы потоков
является использование оператора lock. Пусть вы хотите реализовать шаблон «одиночка». Он позволяет иметь в приложении
только один экземпляр класса. Стандартная реализация этого
шаблона выглядит следующим образом:
public class Singleton { private static Singleton _instance = null; private Singleton() { } public static Singleton Instance { get { if (_instance == null) _instance = new Singleton(); return _instance; } } Поскольку конструктор класса является закрытым, то получить экземпляр класса возможно только через статическое свойство Instance. Внутри этого свойства создается единственный экземпляр класса, который возвращается всем запрашивающим.
Однако данная реализация имеет изъян. Если два потока одновременно вызовут свойство Instance, одновременно проверят, что
_instance == null, им обоим будет указано, что экземпляр еще не
создан. После этого каждому из них будет создан и возвращен
собственный экземпляр класса Singleton, а переменная _instance
будет иметь ссылку на последний созданный объект. Таким образом, в приложении появятся 2 экземпляра класса Singleton. Но это
34
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
не то поведение, которое нам нужно. Поэтому код свойства
Instance нужно модифицировать следующим образом:
private static object _locker = new object(); public static Singleton Instance { get { if (_instance == null) { lock (_locker) { if (_instance == null) { _instance = new Singleton(); } } } return _instance; } } Обратите внимание на использование оператора lock. Поток,
достигнув его, проверит, заблокирован ли объект, переданный
оператору lock в качестве параметра. Если объект уже заблокирован, поток будет ждать до тех пор, пока объект не разблокируется. Если же объект свободен, то поток заблокирует его и будет
выполнять код, имеющийся в блоке lock. При выходе из этого
блока объект, переданный оператору lock, всегда будет разблокирован.
Рассмотрим, как использование lock позволяет решить нашу
проблему. Оба потока проверят _instance и узнают, что экземпляр
Singleton еще не создан. Тогда они перейдут к блоку lock. Только
один из потоков успеет первым заблокировать _locker, а второй
поток останется ждать, пока этот объект не будет разблокирован.
Первый поток успешно создаст экземпляр Singleton и покинет
блок lock. Только в этот момент второй поток сможет войти в этот
35
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
блок. Теперь становится ясна необходимость во второй проверке
переменной _instance внутри блока lock. Если бы этой проверки не
было, второй поток, войдя в блок, создал бы еще один экземпляр
Singleton, что недопустимо.
В заключение рассмотрения работы оператора lock хочется
привести один полезный прием. Иногда у вас нет объекта вроде
_locker, который можно было бы заблокировать. В этом случае
можно использовать следующий синтаксис:
lock (typeof(Singleton)) 3.2. Класс ReaderWriterLock
Использование оператора lock является, наверно, наиболее
простым способом синхронизации доступа к ресурсам. Однако
это не всегда то, что нам нужно. Рассмотрим классический пример использования общего кэша. Обычно кэш реализуется с использованием класса Dictionary.
private static Dictionary<int, int> m_Cache = new Dictionary<int, int>(); Несколько потоков читают данные из кэша, при отсутствии
данных в кэше поток получает их из другого места и записывает
в кэш. Очевидно, что чтение из кэша могут одновременно совершать несколько потоков, поскольку при этом сам кэш не изменяется. В то же время, когда производится запись в кэш, никто
больше не может ни писать, ни читать из него. Конечно, синхронизацию работы потоков с кэшем можно реализовать с помощью
оператора lock, но это не является самым эффективным решением. Дело в том, что с помощью lock нельзя реализовать одновременное чтение из кэша несколькими потоками. Оператор lock
блокирует кэш только для одного потока.
Здесь на помощь приходит класс ReaderWriterLock. Все потоки,
обращающиеся к одному ресурсу, должны использовать один экземпляр этого класса. Данный класс имеет методы получения
доступа к ресурсу AcquireReaderLock и AcquireWriterLock, а также соответствующие методы освобождения – ReleaseReaderLock и
ReleaseWriterLock.
Работа с классом происходит следующим образом. Поток,
который хочет получить доступ только на чтение, вызывает метод AcquireReaderLock. Этому методу в качестве параметра переда36
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
ется время, которое поток должен дожидаться получения соответствующего доступа. Если за это время доступа получить не
удалось, то возникает ApplicationException. Вы так же можете передать этому методу параметр Timeout.Infinite. В этом случае ожидание будет длиться бесконечно. Экземпляр класса ReaderWriterLock
разрешит доступ только в том случае, если у него есть только запросы на чтение, если же есть хотя бы один запрос на запись, то
доступ предоставлен не будет до тех пор, пока все запросы на запись не будут удовлетворены. После завершения чтения поток
должен обязательно вызвать метод ReleaseReaderLock. Этим он сигнализирует, что освободил ресурс. Для использования методов
AcquireReaderLock и ReleaseReaderLock очень подходит блок try–finally:
try { locker.AcquireReaderLock(Timeout.Infinite); if (m_Cache.ContainsKey(key)) Console.WriteLine( "For key {0} reader value is {1}.", key, m_Cache[key]); } finally { locker.ReleaseReaderLock(); } Следует отметить, что класс ReaderWriterLock сам не следит,
производите ли вы только чтение или все же изменяете общий
ресурс. Такое слежение – задача самого программиста.
Поток, которому нужно производить запись, вызывает метод
AcquireWriterLock. Смысл его параметра тот же. Этот метод будет
ждать до тех пор, пока не будут удовлетворены все запросы на
запись и чтение, которые были сделаны до его вызова. Потом он
заблокирует
объект
до
его
освобождения
методом
ReleaseWriterLock. Пока объект заблокирован таким образом, никто
не может получть доступ ни на запись, ни на чтение:
try { locker.AcquireWriterLock(Timeout.Infinite); 37
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
if (m_Cache.ContainsKey(key)) Console.WriteLine("For key {0} reader value is {1}.", key, m_Cache[key]); else { m_Cache[key] = key * key; Console.WriteLine("For key {0} written value is {1}.", key, m_Cache[key]); } } finally { locker.ReleaseWriterLock(); } Обратите внимание, что после получения доступа на запись
поток опять проверяет, есть ли в кэше нужное значение. Это необходимо потому, что, пока поток ждал доступа на запись, другой поток, который получил такой доступ ранее, мог уже записать в кэш нужное значение.
Вот, собственно, и все, что касается использования класса
ReaderWriterLock. Не забывайте освобождать объект после завершения необходимых операций и следите за тем, чтобы не изменять общий ресурс, получив доступ только на чтение. Кроме того, нужно отметить, что теперь в библиотеке стандартных классов присутствует класс ReaderWriterLockSlim. Он выполняет ту же
работу, что и ReaderWriterLock, но имеет существенно лучшее быстродействие. Имена его методов несколько отличаются от имен
методов ReaderWriterLock, но смысл их по-прежнему остается тем
же. Microsoft рекомендует вам использовать именно
ReaderWriterLockSlim в большинстве случаев.
3.3. Класс Mutex Как видно из предыдущего примера, оператор lock и класс
ReaderWriterLock позволяют эффективно производить синхронизацию потоков приложения. Однако иногда требуется синхронизация не только в рамках одного процесса, но и в пределах не38
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
скольких процессов. Классическим примером является создание
приложения, у которого может быть запущен только единственный экземпляр. Несомненно, .NET имеет более гибкие механизмы, обеспечивающие подобное поведение приложения, но мы
рассмотрим простейший способ, подразумевающий использование класса Mutex.
bool isCreated; Mutex mtx = new Mutex(true, "SingleThreadingMutex", out isCreated); if (!isCreated) { Console.WriteLine("Application is already running."); Console.ReadLine(); return; } Console.WriteLine("Application works."); Console.ReadLine(); mtx.Close();
Рассмотрим его применение. Конструктор класса Mutex имеет
несколько перегруженных версий. Используемая нами принимает
3 параметра. Первый из них говорит, должны ли мы сразу получить мьютекс в эксклюзивное пользование (владеть им). В этом
случае класс Mutex создает внутри себя глобальный для операционной системы объект с заданным именем, которое передается в
качестве второго параметра конструктора. Именно потому, что
этот объект является глобальным для операционной системы,
мьютекс годен для синхронизации между процессами. Если такой
объект с этим именем уже существует, то создать его заново не
удастся. Об этом сигнализирует третий параметр конструктора.
Таким образом, только запущенный первым экземпляр приложения сумеет создать мьютекс. Всем остальным это не удастся, и
они получат соответствующее уведомление через переменную
isCreated.
После окончания работы вашего приложения мьютекс нужно
закрыть:
mtx.Close(); 39
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Эта команда уничтожает созданный глобальный объект, и
теперь его можно создавать заново.
Кроме описанных выше возможностей, класс Mutex является
наследником класса WaitHandle и обладает всеми его возможностями.
3.4. Класс WaitHandle
Класс WaitHandle разработан для поддержки потоками ожидания завершения различных задач. Сам этот класс является абстрактным, но имеет несколько полезных методов. К ним относятся
WaitAll, позволяющий дождаться освобождения всех объектов
WaitHandle, переданных ему в массиве в качестве параметра, и
WaitAny, позволяющий дождаться первого освобождения одного
из объектов WaitHandle, переданных ему в массиве в качестве параметра.
Легче всего рассмотреть работу с этим классом на примере
его наследников. Обычно используют два из них – AutoResetEvent
и ManualResetEvent. Эти классы обладают также поддержкой возможности уведомления ожидающих потоков о том, что ожидаемое ими событие произошло.
Класс AutoResetEvent
Рассмотрим следующую задачу. Вы хотите вычислить интеграл некоторой функции. Поскольку на вашем компьютере есть
два процессора, то вы хотели бы решить эту задачу, используя
два потока, благо интегрирование очень легко разделить. Первый
поток будет считать интеграл по первой половине интересуещего
вас отрезка, а второй – по второй половине. Потом вам нужно
просто просуммировать результаты работы этих потоков. Однако
вам необходимо знать, когда оба этих потока завершили свою работу, когда результаты готовы. Для получения уведомления об
этом мы воспользуемся классом AutoResetEvent.
public class Integrator { // Класс отрезка интегрирования private class IntegrationRange { 40
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
public double Start { get; set; } public double Finish { get; set; } } private double firstPartResult = 0; private double secondPartResult = 0; private double tolerance = 0.00000001; private WaitHandle[] handles = new WaitHandle[] { new AutoResetEvent(false), new AutoResetEvent(false) }; public double GetIntegral(double start, double finish) { double center = (start + finish) / 2.0; ThreadPool.QueueUserWorkItem(CalcFirstPart, new IntegrationRange() { Start = start, Finish = center }); ThreadPool.QueueUserWorkItem(CalcSecondPart, new IntegrationRange() { Start = center, Finish = finish }); WaitHandle.WaitAll(handles); return firstPartResult + secondPartResult; } private void CalcFirstPart(object state) { IntegrationRange range = (IntegrationRange)state; firstPartResult = CalcIntegral(range.Start, range.Finish); (handles[0] as AutoResetEvent).Set(); } private void CalcSecondPart(object state) { IntegrationRange range = (IntegrationRange)state; secondPartResult = CalcIntegral(range.Start, range.Finish); (handles[1] as AutoResetEvent).Set(); } 41
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
private double CalcIntegral(double start, double finish) { double i1 = 0, i2 = 0; int n = 100; do { i1 = i2; i2 = 0; for (int i = 0; i < n; i++) { double x = start + (finish ‐ start) * i / n; i2 += (x * x) * (finish ‐ start) / n; } n *= 2; } while (Math.Abs(i1 ‐ i2) > tolerance); return i2; } } Приведенный здесь код класса выполняет данную работу.
Рассмотрим, как он это делает. Его единственный открытый метод – GetIntegral, который фактически запускает в отдельных потоках два метода, которые считают первую и вторую части интеграла:
ThreadPool.QueueUserWorkItem(CalcFirstPart, new IntegrationRange() { Start = start, Finish = center }); ThreadPool.QueueUserWorkItem(CalcSecondPart, new IntegrationRange() { Start = center, Finish = finish }); Далее метод GetIntegral ожидает завершение работы обоих
этих потоков. Для этого он использует метод WaitAll класса
WaitHandle. Как уже говорилось, этот метод ожидает, пока все переданные ему в массиве объекты WaitHandle не сообщат ему, что
они свободны. Тонкостью здесь является только то, что в некото42
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
рых операционных системах этот метод не может работать более
чем с 64 объектами WaitHandle.
Массив, который передается методу WaitAll, содержит объекты AutoResetEvent.
private WaitHandle[] handles = new WaitHandle[] { new AutoResetEvent(false), new AutoResetEvent(false) }; Конструктору этих объектов передается флаг, указывающий,
в каком состоянии они находятся: свободно (true) или занято
(false). В данном случае все объекты AutoResetEvent заняты, и потоку придется ждать их освобождения.
Методы CalcFirstPart и CalcSecondPart вычисляют интегралы и
сигнализируют о завершении своей работы, вызывая метод Set
объектов AutoResetEvent. Этот метод работает следующим образом. Предположим, что есть несколько потоков, которые ждут
освобождения конкретного объекта AutoResetEvent. Вызов метода
Set этого объекта закончит ожидание только одного ждущего потока, остальные потоки продолжат свое ожидание до следующего
вызова метода Set. Если после очередного вызова этого метода не
осталось более ждущих потоков, то объект AutoResetEvent переходит в состояние «свободно». И для того чтобы снова перевести
его в состояние занято, нужно вызвать его метод Reset.
Класс ManualResetEvent
Класс ManualResetEvent является практически полным аналогом класса AutoResetEvent, за исключением одной вещи. Как говорилось ранее, вызов метода Set класса AutoResetEvent освобождает
только один ждущий поток. Вызов же этого метода класса
ManualResetEvent освобождает все ждущие потоки.
Таким образом, вам решать, когда использовать каждый из
этих классов.
43
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
3.5. Блокировка потоков
При реализации синхронизации работы потоков следует соблюдать осторожность. Вполне возможны ситуации, когда потоки будут ждать друг друга и никогда не освободятся. Такие ситуации называются взаимной блокировкой. Рассмотрим следующий пример.
Пусть для каких-то целей нам нужно очень быстро вычислять
значение какой-либо функции. Стандартным приемом в этом
случае является табуляция. Значения функции в нужных точках
вычисляются и затем сохраняются в кэше. Метод GetValue, выполняясь в отдельном потоке, извлекает необходимые ему значения функции из кэша. Если же он не нашел значения в кэше, он
помещает нужый ему аргумент функции в отдельный список.
Другой метод (CalcFunction), выполняющийся также в отдельном
потоке, извлекает аргументы функции из этого списка, вычисляет
значение функции и помещает ее в кэш.
Поскольку и список аргументов, и кэш могут быть изменены
в любой момент, то оба метода используют оператор lock для
синхронизации доступа к этим объектам:
private static void GetValue(object state) { Random rnd = new Random(); while (true) { int number = rnd.Next(1000); lock (squares) { if (squares.ContainsKey(number)) { Console.WriteLine("Square of {0} is {1}.", number, squares[number]); continue; } lock (numbersToProcess) { numbersToProcess.Add(number); 44
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
} } } } private static void CalcFunction(object state) { while (true) { lock (numbersToProcess) { if (numbersToProcess.Count > 0) { int number = numbersToProcess[0]; numbersToProcess.Remove(0); lock (squares) { squares[number] = number * number; } Console.WriteLine("Square for {0} is calculated.", number); } } } } Как видно из кода, метод GetValue сперва захватывает кэш, а
потом список аргументов, метод же CalcFunction, наоборот, сперва
захватывает список аргументов, а затем кэш. Именно поэтому оба
потока взаимно блокируют друг друга. Первый успевает захватить кэш, а второй – список аргументов. Затем они будут ждать
друг друга, пытаясь захватить уже заблокированные другим потоком объекты.
В данном случае проблема решается просто – путем вынесения блоков lock друг из друга. Но в серьезных приложениях проблемы со взаимной блокировкой могут быть менее явны. Поэтому программисту нужно быть очень внимательным при написа45
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
нии многопоточных приложений, синхронизирующих доступ к
ресурсам.
4. Взаимодействие с пользовательским интерфейсом
Последним вопросом, который нам необходимо обсудить,
является взаимодействие отдельного потока с пользовательским
интерфейсом. На самом деле, графический интерфейс пользователя (GUI) может исполняться только в том же потоке, в котором
был создан. О том, как этого добиться, и рассказывает эта глава.
4.1. WinForms
Несмотря на стремительное наступление технологии WPF,
все еще много приложений пишется на WinForms. Начнем наше
рассмотрение взаимодействия потоков с пользовательским интерфейсом в WinForms-приложениях.
Метод Invoke
Наиболее распространенным способом выполнить свой код
гарантированно в основном потоке является использование метода Invoke класса Control. Этот метод гарантированно исполняет код
переданного ему делегата в том потоке, в котором был создан соответствующий элемент Control:
Func<long> dlgt = new Func<long>(GetCheckNumber); return (long) this.Invoke( dlgt ); Просто оберните вызов вашего метода в делегат и передайте
его методу Invoke. Кроме того, класс Control имеет свойство
InvokeRequired, позволяющий узнать, нужно ли вызывать для вашего кода Invoke или можно выполнять его напрямую. В связи с
этим полный код метода, который должен выполняться в потоке
пользовательского интерфейса, выглядит следующим образом:
private long GetCheckNumber() { if (this.InvokeRequired) { Func<long> dlgt = new Func<long>(GetCheckNumber); return (long) this.Invoke( dlgt ); } 46
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
else { return long.Parse(tbNumber.Text); } } Следует отметить, что вызов таких методов из вашего рабочего потока не должен быть слишком частым, поскольку в этом
случае окажется, что вы постоянно нагружаете поток пользовательского интерфейса и у него не остается времени на свои задачи (например, на отрисовку). Это будет эквивалентно выполнению всего вашего кода в одном потоке, чего мы хотели избежать.
Кроме того, слишком частый вызов Invoke замедляет выполнение
вашего потока.
Использование SynchronizationContext
Метод Invoke является наиболее распространенным способом
синхронизации пользовательского интерфейса с отдельным потоком. Однако у него есть недостаток, иногда ограничивающий его
применимость. Дело в том, что для его работы элемент, у которого он вызывается, должен быть отображен на экране. Говоря более строго, у него должен быть действительный (рабочий, валидный) handle окна, т. е. элемент должен быть не только создан конструктором, но и отображен. Это не всегда возможно. В данном
случае на помощь приходит класс SynchronizationContext. Получить
объект этого класса очень просто. Для этого используется его
статическое свойство Current. Оно возвращает синхронизационный контекст потока. Однако знайте, что у потока может не быть
синхронизационного контекста. Поэтому это свойство может
возвращать и null. Однако метод Application.Run, запускающий
WinForms-приложение, всегда устанавливает этот контекст для потока пользовательского интерфейса. Поэтому вы можете безопасно получить его, обычно это делается по событию загрузки
главной формы:
private void MainForm_Load(object sender, EventArgs e) { m_SyncContext = SynchronizationContext.Current; } 47
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Вы должны сохранить ссылку на синхронизационный контекст, чтобы он был доступен в вашем отдельном потоке.
Синхронизационный контекст имеет два основных метода –
Send и Post. Обоим передается делегат SendOrPostCallback и параметр для него. Первый выполняет переданный ему делегат синхронно (блокируя вызвавший метод Send поток до тех пор, пока
делегат не будет исполнен), а второй – асинхронно.
Применение этих методов может выглядеть примерно таким
образом:
m_SyncContext.Send( new SendOrPostCallback( delegate { pb.Minimum = 0; pb.Maximum = 100; pb.Value = 0; btnCheck.Enabled = false; } ), null ); Использование анонимных методов позволяет избавиться от
необходимости писать отдельные методы и передавать им параметры.
Класс BackgroundWorker
Последним способом взаимодействия пользовательского интерфейса и отдельного потока является использование класса
BackgroundWorker. Это компонент, который можно поместить на
форму, перетащив мышкой из палитры компонентов. Какие же
возможности он нам предоставляет?
Основным для класса BackgroundWorker является событие
DoWork. Обработчик этого события вы пишете сами, именно он
будет исполняться в отдельном потоке. Запуск отдельного потока
осуществляется вызовом метода RunWorkerAsync экземпляра класса BackgroundWorker. Этому методу можно передать параметр типа
object, в который можно упаковать любые данные, необходимые
вашему потоку:
48
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
long checkNumber = GetCheckNumber(); backgroundWorker.RunWorkerAsync(checkNumber); В вашем обработчике события DoWork эти данные доступны
через свойство Argument параметра типа DoWorkEventArgs:
long checkNumber = (long) e.Argument; Окончание работы и возвращение результата
Если ваш метод, выполняющийся в отдельном потоке, должен
возвращать какой-то результат, присвойте этот результат свойству
Result объекта DoWorkEventArgs. Тогда он будет доступен после завершения работы вашего метода. Об окончании его работы уведомляет событие RunWorkerCompleted класса BackgroundWorker:
private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { pb.Minimum = 0; pb.Maximum = 100; pb.Value = 0; btnCheck.Enabled = true; if (e.Error != null) { MessageBox.Show(this, e.Error.Message, "Ошибка"); return; } if (e.Cancelled) { MessageBox.Show(this, "Пользователь отменил выполнение метода.", "Информация"); return; } bool isPrime = (bool) e.Result; if (isPrime) { MessageBox.Show(this,"Число является простым."; 49
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
} else { MessageBox.Show(this,"Число не является простым."); } }
Достоинство этого метода в том, что он выполняется в потоке пользовательского интерфейса, поэтому вам нет нужды заботиться о синхронизации. Данному методу передается параметр
типа RunWorkerCompletedEventArgs, который имеет следующие полезные свойства:
1) если в вашем потоке произошло необработанное исключение, вы можете получить его с помощью свойства Error. Более того, вы обязаны сделать это, поскольку обращение к любым другим свойствам объекта RunWorkerCompletedEventArgs вызовет генерацию исключения;
2) если пользователь прервал работу вашего метода (об этом
позже), вы можете узнать об этом из свойства Cancelled;
3) наконец, вы можете получить результат работы вашего метода через свойство Result.
Прогресс выполнения
Вывод на экран прогресса выполнения длительной операции
является стандартной задачей. Класс BackgroundWorker для этих
целей имеет метод ReportProgress. Ему передается целое число, которое указывает процент выполненой работы.
backgroundWorker.ReportProgress((int)(100.0 * 2 * d / checkNumber)); Этот метод приводит к вызову события ProgressChanged, обработчик которого вы можете написать самостоятельно:
private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { pb.Value = e.ProgressPercentage; } В данном обработчике вы можете получить переданный процент
с
помощью
свойства
ProgressPercentage
класса
50
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
ProgressChangedEventArgs. Этот метод также выполняется в потоке
пользовательского интерфейса, так что вы можете не использовать
Invoke.
Отмена выполнения метода
Отмена выполнения задачи организована в BackgroundWorker
очень похоже на использование токенов отмены. Для этой цели
класс BackgroundWorker предоставляет метод CancelAsync. Вызов
данного метода приводит к выставлению в true свойства
CancellationPending объекта BackgroundWorker. Программист в обработчике события DoWork может (но не обязан) проверять это
свойство. Если оно истинно, программист может некоторым образом досрочно завершить работу потока. Также он может установить в true свойство Cancel объекта DoWorkEventArgs, чтобы в обработчике события RunWorkerCompleted стало известно, что работа
прервана пользователем:
if (backgroundWorker.CancellationPending) { e.Cancel = true; return; } Вот, собственно, и все, что касается работы с классом
BackgroundWorker. Его преимущество заключается в том, что он
берет на себя практически всю работу по синхронизации отдельного потока с пользовательским интерфейсом. Его события прогресса и окончания работы исполняются в потоке пользовательского интерфейса, так что программист может не заботиться сам
о синхронизации. Кроме того, BackgroundWorker предлагает
удобные механизмы прерывания работы, уведомления о завершении, получения результатов и возникших ошибок. Поэтому
при работе с GUI этот класс следует использовать везде, где
только возможно.
4.2. WPF
С версии 3 .NET Framework поддерживает новую технологию
для создания пользовательских интерфейсов – WPF (Windows
Presentation Foundation). В принципе с точки зрения взаимодейст51
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
вия с многопоточностью эта технология мало чем отличается от
WinForms. Точно так же изменение состояние элементов управления WPF допускается только из потока пользовательского интерфейса. Рассмотрим существующие различия.
Объект Dispatcher
Все объекты, для которых важно то, чтобы их изменение
происходило из того же потока, в котором они были созданы, являются наследниками класса DispatcherObject. Данный класс имеет
два метода CheckAccess и VerifyAccess. Оба они проверяют, выполняется ли текущий код в том же потоке, в котором был создан
объект. CheckAccess возвращает булевское значение, а VerifyAccess
генерирует исключение, если код выполняется не в том потоке.
Все элементы управления WPF унаследованы от DispatcherObject и
часто используют VerifyAccess для проверки того, правильно ли
производятся их изменения.
Кроме того, объект DispatcherObject имеет свойство Dispatcher,
возвращающее объект Dispatcher, который можно использовать
для выполнения вашего кода в потоке пользовательского интерфейса. Объект Dispatcher имеет метод Invoke, работающий совершенно так же, как и аналогичный метод элементов управления
WinForms.
private long GetCheckNumber() { if (!this.CheckAccess()) { Func<long> dlgt = new Func<long>(GetCheckNumber); return (long)this.Dispatcher.Invoke(dlgt); } else { return long.Parse(tbCheckNumber.Text); } } 52
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Вместо InvokeRequired вы используете CheckAccess, а метод
Invoke вызываете у объекта, доступного через свойство Dispatcher.
Вот и все различия.
Класс BackgroundWorker
WPF также предоставляет свою реализацию класса
BackgroundWorker. Она практически не отличается от аналогичного
класса в WinForms.
5. Задание для лабораторной работы 2
В соответствии с вариантом задания самостоятельно написать многопоточное приложение.
На входе – имя диска/каталога. Первый поток просматривает
исходный каталог и все вложенные подкаталоги, записывает в
очередь все файлы, которые там хранятся (как вариант – файлы
какого-то определенного типа). Второй поток последовательно
берет файлы из очереди, считает у каждого файла некоторый код
(по варианту) и выводит на консоль результат: имя файла – значение суммы. Синхронизировать потоки, чтобы они не блокировали друг друга при обращении к очереди.
Варианты заданий
№ Создание потоков
Код
с помощью
1. Делегата
сумма по модулю два всех байтов
файла 2. Класса Thread
сумма по модулю два всех нечетных байтов файла
3. Класса ThreadPool сумма по модулю два первого и
последнего байтов файла
4. Класса Task
остаток от деления на 7 суммы
второго и четвертого байтов
5. Делегата
сумма по модулю два всех четных
байтов файла
6. Класса Thread
остаток от деления на 5 суммы
первых четырёх байтов
7. Класса ThreadPool остаток от деления на 3 суммы
последних трёх байтов файлов
8. Класса Task
остаток от деления на 9 суммы
первого и третьего байтов файла
53
Синхронизация с
помощью
Lock
Reader/WriterLock
Lock
Reader/WriterLock
Lock
Reader/WriterLock
Lock
Reader/WriterLock
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
9.
Делегата
сумма по модулю два первых
10 байтов файла
10. Класса Thread
сумма по модулю два трёх последних байтов файла
11. Класса ThreadPool остаток от деления на 4 суммы
второго и четвертого байтов
12. Класса Task
сумма по модулю два всех четных
байтов файла
Lock
Reader/WriterLock
Lock
Reader/WriterLock
Список литературы
1. Троелсен, Э. С# и платформа .Net 3.0, специальное издание
/ Э. Троелсен. – СПб.: Питер, 2008. – 1456 с.
2. Рихтер, Д. CLR via C#. Программирование на платформе
Microsoft .Net Framework 2.0 на языке C#: Мастер-класс
/ Д. Рихтер. – СПб.: Питер, 2007. – 656 с.
3. Нортроп, Т. Основы разработки приложений на платформе
Microsoft .Net Framework: учебный курс Microsoft / Т. Нортроп,
Ш. Уилдермьюс, Б. Райан. – СПб.: Питер, 2007. – 864 с.
4. Главная страница MSDN. – URL: http://www.msdn.com
54
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Оглавление
Лабораторная работа 1. Сериализация ........................................................... 3
Необходимые теоретические сведения ..................................................... 3
1. Понятие сериализации объектов ........................................................... 3
2. Настройка объектов для сериализации ................................................. 4
3. Выбор форматера сериализации ............................................................ 6
4. Поддержка версий объектов ................................................................ 11
5. Настройка сериализации с использованием атрибутов .................... 12
6. Задание для лабораторной работы 1 ................................................... 13
Лабораторная работа 2. Многопоточные приложения .............................. 13
Необходимые теоретические сведения ................................................... 13
1. Задачи многопоточности ...................................................................... 13
2. Методы создания потоков .................................................................... 15
2.1. Делегаты .................................................................................... 15
2.3. Класс ThreadPool....................................................................... 24
2.4. Класс Task .................................................................................. 26
2.5. PLINQ ......................................................................................... 31
3. Синхронизация потоков ....................................................................... 33
3.1. Оператор lock ........................................................................... 34
3.2. Класс ReaderWriterLock ............................................................ 36
3.3. Класс Mutex ................................................................................ 38
3.4. Класс WaitHandle ...................................................................... 40
3.5. Блокировка потоков ................................................................. 44
4. Взаимодействие с пользовательским интерфейсом .......................... 46
4.1. WinForms .................................................................................... 46
4.2. WPF ............................................................................................ 51
5. Задание для лабораторной работы 2 ................................................... 53
Список литературы......................................................................................... 54
55
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Учебное издание
Якимова Ольга Павловна
Якимов Иван Михайлович
Дольников Владимир Леонидович
Языки программирования
Часть 2
Лабораторный практикум
Редактор, корректор М. В. Никулина
Верстка И. Н. Иванова
Подписано в печать 16.04.12. Формат 6084 1/16.
Бум. офсетная. Гарнитура "Times New Roman".
Усл. печ. л. 3,25. Уч.-изд. л. 2,0.
Тираж 27 экз. Заказ
.
Оригинал-макет подготовлен в редакционно-издательском отделе
Ярославского государственного университета
им. П. Г. Демидова.
Отпечатано на ризографе.
Ярославский государственный университет
им. П. Г. Демидова.
150000, Ярославль, ул. Советская, 14.
56
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
57
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
58
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
О. П. Якимова
И. М. Якимов
В. Л. Дольников
Языки программирования
Часть 2
59
Документ
Категория
Без категории
Просмотров
34
Размер файла
573 Кб
Теги
язык, якимова, 547, программирование
1/--страниц
Пожаловаться на содержимое документа