NET программирование
Главная Новости
Framework .NET
Visual Studio .NET Самоучитель C# Учебники О сайте
Что такое Microsoft .NET?
Основы технологии .NET
Программирование на управляемом C++
Объектно-ориентированное программирование на управляемом C++
Управляемый C++ в .NET Framework
Создание графических пользовательских интерфейсов
Сборки и развертывание
Классы каркаса .NET Framework
Программирование в ADO.NET
ASP.NET и Web-формы
Web-службы
Web-узлы и Web-службы,работающие на основе ATL Server
Защита
Трассировка и отладка в .NET
Смешивание управляемого и неуправляемого кода
Приложение. Visual Studio.NET
Классы каркаса .NET Framework

Невозможно описать в одной главе, или даже в одной книге, все классы каркаса .NET Framework. Хотя и не полностью, классы .NET охватывают большую часть интерфейса 32-разрядных Windows-приложений (Win32 API), так же как и много чего другого. Несмотря на то, что основное внимание было уделено функциональным возможностям, связанным с Internet, однако изменилась и модель разработки приложений в среде Windows.

В этой главе мы сосредоточим наше внимание на классах, которые иллюстрируют ключевые концепции и модели, проявляющиеся повсюду в каркасе .NET Framework. Такой подход представляется нам более плодотворным, чем просто попытаться немного рассказать о каждом классе, который когда-либо мог бы понадобиться, без того, чтобы дать читателю общее представление о классах .NET. В других главах глубже рассматриваются иные части каркаса, такие как Windows Forms (Формы Windows), ASP.NET, безопасность ADO.NET, и сетевые службы (Web Services).

Мы начинаем с рассмотрения концепций отражения и метаданных. Метаданные появляются всюду в .NET и важно понимать, как общеязыковая среда времени выполнения CLR (Common Language Runtime) предоставляет разные услуги (службы, сервисы) прикладным программам. Затем по нескольким причинам мы исследуем файловый ввод/вывод. Во-первых, при этом вводится важное понятие сериализации (преобразования в последовательную форму). Во-вторых, класс Path (Путь) позволяет проиллюстрировать, как отдельные классы каркаса реализуют некоторые или все свои функциональные возможности с помощью статических методов. В-третьих, рассматриваемые в применении к файловому вводу/выводу классы используются для форматирования во многих местах .NET.

Разобравшись в понятии сериализации, читатель получит конкретную идею относительно того, как каркас может управлять объектами прозрачным для нас способом. Ведь сериализация появляется во вспомогательной роли в любом месте, где объекты должны будут сохраняться или транспортироваться. Обсуждение интерфейса ISerializable снова демонстрирует, насколько проще реализовать интерфейс, используя .NET, чем с помощью модели компонентных объектов Microsoft (COM).

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

Для лучшего понимания модели программирования .NET, мы введем понятие контекста, а также рассмотрим применение заместителей и заглушек для реализации системных сервисов. Мы также рассмотрим использование прикладных областей, благодаря которым достигается более эффективная изоляция приложений, чем с помощью процессов Win32.

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

Пример Serialization (Сериализация) из главы 2 "Основы технологии .NET" демонстрирует, как благодаря метаданным общеязыковая среда времени выполнения CLR поддерживает работу сервисов. Многие из технологий, которые мы рассматриваем в других главах книги, основаны на метаданных, хотя мы не всегда будем акцентировать на этом внимание.

Метаданные — это информация о сборках, модулях и типах, составляющих программы в .NET. Тот, кому когда-либо приходилось создавать язык описания интерфейса (IDL) с целью сгенерировать библиотеку типов так, чтобы созданные с помощью C++ СОМ-объекты могли вызываться из Visual Basic, или создавать заместители и заглушки, оценит, насколько полезными являются метаданные, и будет благодарен, что распространяется это все "бесплатно".

Компиляторы генерируют метаданные, а общеязыковая среда времени выполнения CLR, каркас .NET Framework и наши собственные программы могут их использовать. Чтобы лучше разобраться в том, как работают метаданные, мы сосредоточимся на обсуждении их использования, а не на их создании. Метаданные можно прочитать с помо-. шью классов из пространства имен System: : Reflection (Система::Отражение).

Когда загружаются сборка и связанные с ней модули и типы, метаданные подгружаются вместе с ними. Затем можно сделать запрос к сборке, чтобы получить связанные с ней типы. Можно также вызвать метод GetType для любого из типов общеязыковой среды времени выполнения CLR и получить его метаданные. GetType — это метод класса System: :Object (Система::Объект), производным от которого является каждый тип общеязыковой среды времени выполнения CLR. После того, как получен Туре (Тип), связанный с объектом, можно использовать методы отражения, чтобы получить соответствующие метаданные.

Программа-пример Reflection (Отражение) берет изучаемую сборку Customer (Клиент) и распечатывает некоторые из доступных метаданных. В следующих разделах книги вы изучите распечатку и исходный код. Особенно важно сравнить вывод программы с исходным кодом в файле customer. h.

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

Assembly *a = Assembly::Load(assemblyName); // Загрузка
Console::WriteLine (
"Assembly {0} found.", a->FullName);

Вывод для этого оператора соответствует неподписанной сборке:

Assembly Customer, Version=l.О.643.18973, Culture=neutral,
PublicKeyToken=null found.

CodeBase — одно из свойств класса Assembly; оно обсуждалось в главе 7 "Сборки и развертывание". Подтверждение защиты, связанное с этой сборкой, — это еще одно свойство. Следующий код пробует определить точку входа сборки:

Methodlnfo *entryMethod!nfo = a->EntryPoint;

Так как это типичный написанный на C++ компонент сборки, то его точка входа — _DllMainCRTStartup@12. Если бы он был исполняемой программой, мы могли бы использовать метод Invoke (Вызвать) класса Methodlnfo, чтобы выполнить код инициализации сборки.

В данном примере для поиска связанных с этой сборкой модулей используется метод GetModules сборки. В данном случае мы имеем только один модуль, customer.dll. Затем мы могли бы найти типы, связанные с этим модулем. Вместо этого мы используем метод GetTypes сборки, чтобы возвратить массив типов сборки.

Абстрактный класс Туре (Тип) в пространстве имен System (Система) определяет типы .NET. Поскольку в .NET нет никаких функций вне классов или глобальных переменных, то получив все типы в сборке, мы получим все метаданные о коде в этой сборке. Туре (Тип) представляет все типы, имеющиеся в .NET: классы, структуры, интерфейсы, значения, массивы и перечисления.

Класс Туре (Тип) возвращается также методом GetType класса System::0bject (Система::Объект) и статическим методом GetType самого класса Туре (Тип). Последний метод может использоваться только с типами, которые могут быть разрешены статически.

Одно из свойств класса Туре (Тип) — сборка, к которой он принадлежит. Можно получить все типы, содержащиеся в сборке, как только будет определен Туре (Тип) одного объекта. Туре (Тип) — абстрактный класс, и во время выполнения возвращается экземпляр System::RuntimeType.

В выдаче программы найден каждый тип в сборке, — CustomerListltem, ICustomer, Customer (Клиент) и Customers (Клиенты),— причем распечатаны его метаданные. Чтобы для каждого типа выяснить стандартные атрибуты и тип, производным от которого является класс, нужно воспользоваться свойствами Attributes (Атрибуты) и BaseType.

Методы, связанные с классом Туре (Тип), дают возможность получить ассоциированные поля, свойства, интерфейсы, события, и методы. Например, тип Customer (Клиент) не имеет никаких интерфейсов, свойств и событий, но имеет четыре поля, три конструктора и методы, унаследованные от его базового класса BaseType из пространства имен System::Object (Система::Объект):

Можно также загрузить и выполнить сборку с AppDomain, — мы обсудим этот вариант в данной главе позже.

Interfaces: Fields:
Customerld
FirstName
LastName
EmailAddress Properties:
Events:
Constructors:
public .ctor(System.String first, System.String last,
System.String email)
public .ctor()
public .ctor(System.Int32 id) Methods:
public Int32 GetHashCodeO
public Boolean Equals(System.Object obj)
public String ToStringO
public Type GetType()

Перевод такой:

Интерфейсы: Поля:
Customerld
FirstName
LastName
EmailAddress Свойства:
События:
Конструкторы:
public .ctor(System.String first, System.String last,
System.String email)
// электронная почта
public.ctor ()
public .ctor(System.Int32 id) Методы:
public Int32 GetHashCodeO
public Boolean Equals(System.Object obj)
// Равняется
public String ToString()
public Type GetType()

Тип Customers (Клиенты) наследуется от одного интерфейса и содержит один конструктор и четыре своих собственных метода в дополнение к четырем уже унаследованным от его базового класса BaseType из пространства имен System: :Object (Система-Объект):

Interfaces:
ICustomer
Fields:
Properties:
Events:
Constructors:
public .ctor()
Methods:
public Void ChangeEmailAddress(System.Int32 id,
System.String emailAddress)
public ArrayList GetCustomer(System.Int32 id)
public Void UnregisterCustomer(System.Int32 id)
public Int32 RegisterCustomer(System.String firstName,
System.String lastName, System.String emailAddress)
public Int32 GetHashCode()
public Boolean Equals(System.Object obj )
public String ToString ()
public Type GetTypeO()

Перевод такой:

Интерфейсы:
ICustomer
Поля:
Свойства:
События:
Конструкторы:
public .ctor() Методы:
public Void ChangeEmailAddress(System.Int32 id,
System.String emailAddress)
public ArrayList GetCustomer(System.Int32 id)
public Void UnregisterCustomer(System.Int32 id)
public Int32 RegisterCustomer(System.String firstName,
System.String lastName, System.String emailAddress)
public Int32 GetHashCode()
public Boolean Equals(System.Object obj )
public String ToString()
public Type GetType()

Вся эта информация была получена с помощью методов Getlnterfaces, GetFields, GetProperties, GetEvents, GetConstructors И GetMethods класса Type (Тип). Поскольку интерфейс — тип, Getlnterfaces возвращает массив объектов Туре (Тип), представляющий интерфейсы, унаследованные или реализованные запрошенным типом Туре (Тип). А так как поля, свойства, события, и методы — не типы, их методы средств доступа не возвращают объекты Туре (Тип). Каждый из их методов доступа возвращает соответствующий класс: Fieldlnfo, Propertylnfo, Eventlnfo, Constructorlnfо и Methodlnf о. Все эти классы, а также класс Туре (Тип), — производные от класса Memberlnfo, который является абстрактным базовым классом для элементов метаданных.

Давайте рассмотрим некоторые из метаданных, связанные с методом класса. Используя методы отражения, мы можем восстановить сигнатуры для всех классов и интерфейсов в сборке Customer (Клиент). Вот распечатка для методов класса Customer (Клиент):

public Void ChangeEmailAddress(System.Int32 id,
System.String emailAddress)
public ArrayList GetCustomer(System.Int32 id)
public Void UnregisterCustomer(System.Int32 id)
public Int32 RegisterCustomer(System.String firstName,
System.String lastName, System.String emailAddress)
public Int32 GetHashCode()
public Boolean Equals(System.Object obj)
public String ToString()
public Type GetType()

Вот код, с помощью которого была получена эта распечатка:

for (int j = 0; j < methodlnfо.Length; j++)
{
if (methodlnfo[j]->IsStatic)
Console::Write (" static "); // статический
if (methodlnfo[j]->IsPublic)
Console::Write(" public ");
if (methodlnfo[j]->IsFamily)
Console::Write(" protected "}; // защищенный
if (methodlnfo[j]->IsAssembly)
Console::Write(" internal "); // внутренний
if (methodlnfо[j]->IsPrivate)
Console::Write(" private "); // частный
Console::Write(
"{0} ", methodlnfo[j]->ReturnType->Name); // Имя
Console::Write( // Запись
"{0}(", methodlnfo[j]->Name); // Имя
Parameterlnfo *paramlnfo [] =
methodlnfo[j]->GetParameters(};
long last = paramlnfo->Length - 1;
for (int k = 0; kLength; k++)
{
Console::Write( // Запись
"{0} {1}",
paramlnfо[k]->ParameterType,
paramlnfо[k]->Name); // Имя
if (k != last) // если не последний
Console::Write(", "); // Запись
}
Console::WriteLine(")");
}

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

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

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

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

Отражение может также использоваться для реализации динамического связывания. Динамическое связывание состоит в том, что метод, который нужно вызвать, определяется в процессе выполнения, а не на этапе компиляции. Это один из примеров того, как метаданные используются для предоставления функциональных возможностей. Предыдущий пример демонстрирует, как получить сигнатуру метода, связанного с типом. Объект Methodlnf о содержит все необходимые метаданные для метода класса. Пример Dynamic (Динамический) демонстрирует очень простой случай динамического связывания.

Мы динамически загружаем сборку и получаем метаданные для метода определенного типа:

// Загрузить (Load) сборку Customer (Клиент)
Assembly *a = Assembly::Load("Customer") ;
// Загрузка ("Клиент")
// Получить метаданных для класса Customers (Клиенты)
// и одного метода
Type *t = a->GetType("01.NetCpp.Acme.Customers"); // Клиенты
Methodlnfо *mi = t->GetMethod("GetCustomer");

Вот что следует помнить программистам при программировании на C++. Работая со строками, которые содержат пространства имен или классы, необходимо использовать должным образом отформатированные строки, которые понятны методам класса отражения. Например, полностью определенное имя класса в вышеописанном коде — OI.NetCpp.Acme.Customers, а не OI::NetCpp::Acme::Customers, отформатированное в стиле C++. Таким образом, используемый формат подобен формату в С#, а не в C++.

Применяя классы отражения, мы могли бы сделать все это полностью динамически, произвольно выбирая типы, методы, и конструкторы из сборки Customer (Клиент), используя методы последнего примера, но мы хотели сохранить пример Dynamic (Динамический) простым. Более честолюбивая программа могла бы делать что-нибудь гораздо более интересное, вроде реализации декомпилятора сборки, который непосредственно из откомпилированной сборки генерирует исходный текст на управляемом C++, С#илиУВ.НЕТ.

Пространство имен System (Система) содержит класс Activator (Активатор, Модуль активизации), в котором перегружается метод Createlnstance, предназначенный для создания экземпляров любого типа .NET с помощью подходящего конструктора. Класс Activator (Активатор, Модуль активизации) рассматривается в этой главе в разделе, посвященном удаленному доступу. Чтобы создать экземпляр объекта Customers (Клиенты), мы вызываем конструктор без параметров.

Type *t = a->GetType("01.NetCpp.Acme.Customers"); // Клиенты
Object *customerlnstance = // Объект
Activator::Createlnstance(t); // Активатор

Затем, чтобы вызвать метод GetCustomer, формируем список параметров и используем метод Invoke (Вызвать) экземпляра Methodlnfо.

Dynamic\Customer\Debug в папку DynamicXDebug перед выполнение Dynamic . ехе.

// вызвать метод
Object *arguments [] = new Object*[1]; // новый Объект
int customerld = -1;
arguments[0] = _box(customerld); // параметры
Object *returnType = mi->Invoke( // Вызвать
customerlnstance, arguments); // параметры

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

if (returnType->GetType() ==
Type::GetType("System.Collections.ArrayList"))
// ("Система.Коллекции.Список массивов")
{
ArrayList *arrayList =
dynamic_cast(returnType);
for (int i = 0; iCount; i++) // Счет
{
Type *itemType =
arrayList->get_Item(i)->GetType();
Fieldlnfo *fi [] = itemType->GetFields();
for (int j = 0; j < fi->Length; j++)
{
Object *fieldValue = // Объект
fi[j]->GetValue(arrayList->get_Item(i));
Console::Write( // Запись
"{0, -10} = {1, -15}",
fi[j]->Name, fieldValue); // Имя
}
Console::WriteLine();
}
}

Снова обращаем внимание на то, что в строке System.Collections.ArrayList (Система.Коллекции.Список массивов) для отделения имен использованы точки, а не "двойные двоеточия.

В этом коде не использованы никакие определенные объекты или типы из сборки Customer (Клиент). С целью проиллюстрировать главные принципы, мы применили некоторые знания о сборке, чтобы код был простым. Однако должно быть понятно, как сделать его полностью общим.

Можно сделать шаг вперед и использовать классы, которые генерируют метаданные (в System: :Reflection: :Emit (Система-Отражение-Генерация)). Можно даже динамически создавать сборку в памяти, а затем загружать и выполнять ее!

Грубо обобщая, можно разделить функции ввода/вывода в каркасе .NET Framework на две широких категории, не зависящих от устройства хранения данных (диск, память, и т.д.): это запись и чтение.

Данные могут рассматриваться как поток байтов или символов. Например, мы могли прочитать 500 байтов из файла, и записать их в буфер памяти. Данные также можно рассматривать как набор объектов. Чтение и запись объектов называются соответственно преобразованием из последовательной формы в параллельную и преобразованием в последовательную форму (сериализацией). Мы можем сериализировать (записать) список объектов типа Customer (Клиент) на диск. Затем мы можем список объектов Customer (Клиент) преобразовать из последовательной формы в параллельную, т.е. прочитать этот список обратно в память.

В пространстве имен System::IO есть несколько классов для чтения и записи, позволяющих использовать различные устройства хранения, если только данные можно трактовать как байты или символы. Функциональные возможности сериализации могут проявляться в различных местах каркаса .NET. Пространство имен System::Runtime::Serialization (Система-Время выполнения::Сериализация) используется для сериализации в общей системе типов (Common Type System). Пространство имен System::Xml: Serialization (Система::Хml::Сериализация) используется для сериализации XML-документов.

Stream (Поток, Абстрактный последовательный файл) — абстрактный класс, который является базовым для чтения и записи байтов в некоторое хранилище данных типа файла. Этот класс поддерживает синхронные и асинхронные чтение и запись. Асинхронные методы обсуждаются позже в данной главе. Класс Stream (Поток, Абстрактный последовательный файл) содержит типичные и вполне ожидаемые от такого класса методы: Read (Чтение), Write (Запись), Seek (Поиск), Flush (Дозапись) и Close (Закрыть).

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

У класса Stream (Поток, Абстрактный последовательный файл) есть и другие производные классы: MemoryStream, BufferedStream и NetworkStream (в System: :Net: : Sockets (Система::Сеть::Сокеты)).

Пример FileStream (в папке FilelO с примерами ввода/вывода) иллюстрирует, как использовать потоковый класс Stream (Поток, Абстрактный последовательный файл). Если файл не существует, создается новый, а потом в этот файл записываются числа от 0 до 9. Если файл уже существует, программа читает 5 байтов в конце файла и затем выводит их на консоль. (Пример нужно выполнить дважды. Первый раз программа создаст файл и запишет в него числа, а во второй раз прочитает и распечатает часть файла).

unsigned char data _gc[] =
// сборщик мусора - данные - символы без знака
new unsigned char _gc [10];
// сборщик мусора - новый символ без знака
FileStream *fs = new FileStream(
"FileStreamTest.txt",
FileMode::OpenOrCreate);
if (fs->Length == 0)
// если Длина == 0
{
Console::WriteLine("Writing Data...");
// Запись данных
for (short i = 0; i < 10; i++) data[i] = (unsigned char)i;
// данные = символ без знака
fs->Write(data, 0, 10);
// Запись данных
}
else
{
fs->Seek(-5, SeekOrigin::End);
// Ищем конец
int count = fs->Read(data, 0, 10);
// Чтение данных
for (int i = 0; i < count; i++)
// счет
{
Console::WriteLine(data[i]);
// данные
}
}
fs->Close();

Классы, производные от Stream (Поток, Абстрактный последовательный файл), целесообразно использовать тогда, когда нужно читать или писать байты данных блоками. Если необходимо прочитать в поток или записать из потока простой тип, такой как Boolean (булев, логический), String (Строка) или Int32, следует использовать классы BinaryReader и BinaryWriter. Пример Binary (Двоичный) в папке FilelO показывает, как использовать эти классы. Нужно создать соответствующий поток (FileStream в примере) и передать его в качестве параметра в конструктор BinaryReader или BinaryWriter. Потом можно использовать один из перегруженных методов Read (Чтение) или Write (Запись) для чтения данных из потока или записи данных в поток. (Причем пример опять нужно выполнить дважды. Сначала файл создается и в него записываются данные. Во второй разданные читаются из файла.)

FileStream *fs = new FileStream(
"BinaryTest.bin", FileMode::OpenOrCreate);
if (fs->Length == 0) // если Длина == 0
{
Console::WriteLine("Writing Data...");
// Запись данных
BinaryWriter *w = new BinaryWriter(fs);
for (short i = 0; i < 10; i++) w->Write(i);
// Запись
w->Close () ;
}
else
{
BinaryReader *r = new BinaryReader(fs);
for (int i = 0; i < 10; i++)
Console::WriteLine(r->ReadInt16());
r->Close();
}
fs->Close();

В абстрактных классах TextReader и TextWriter данные рассматриваются как последовательный поток символов (то есть, просто как текст). TextReader имеет следующие методы: Close (Закрыть). Peek (Считывание элемента данных), Read (Чтение), ReadBlock, ReadLine и ReadToEnd. TextWriter содержит методы типа Close (Закрыть), Flush (Дозапись), Write (Запись) и WriteLine. Перегруженные методы Read (Чтение) читают символы из потока. Перегруженные методы Write (Запись) и WriteLine записывают данные различных типов в поток. Если объект записывается в поток, то используется метод ToString объекта.

StringReader и StringWriter являются производными классами от классов TextReader и TextWriter соответственно. StringReader и StringWriter читают и записывают данные в символьную строку, которая сохраняется в базовом объекте StringBuilder. Конструктор StringWriter может принимать объект StringBuilder. Класс StringBuilder обсуждался в главе 3 "Программирование на управляемом C++".

StreamReader и StreamWriter также являются производными классами от классов TextReader и TextWriter. Они читают текст из объекта и записывают текст в объект Stream (Поток, Абстрактный последовательный файл). Так же как и в случае классов BinaryReader и BinaryWriter, можно создать объект Stream (Поток, Абстрактный последовательный файл) и передать его в конструктор StreamReader или StreamWriter. Следовательно, эти классы могут использовать любые унаследованные от Stream (Поток, Абстрактный последовательный файл) классы хранения данных. Пример Text (Текст) из папки File использует классы StreamReader и StreamWriter. Программу необходимо выполнить дважды: первый раз — чтобы создать файл, а затем второй раз — чтобы прочитать его.

FileStream *fs = new FileStream(
"TextTest.txt", FileMode::OpenOrCreate);
if (fs->Length == 0) // если Длина == О {
Console::WriteLine("Writing Data..."); // Запись данных
StreamWriter *sw = new StreamWriter(fs);
sw->Write(100); // Запись
sw->WriteLine(" One Hundred"); // Сто
sw->WriteLine("End of File"); // Конец Файла
sw->Close();
}
else
{
String *text; // Строка
StreamReader *sr = new StreamReader(fs) ;
text = sr->ReadLine(); // текст
while (text != 0)
// пока (текст!= О)
{
Console::WriteLine(text);
// текст
text = sr->ReadLine();
// текст
}
sr->Close ();
}
fs->Close ();

Каркас содержит два класса File (Файл) и File Info, которые очень полезны для работы с файлами. Класс File (Файл) предоставляет основные функциональные возможности для обработки файлов в дополнение к операциям чтения и записи. Поскольку класс File (Файл) содержит только статические члены, имя файла необходимо в качестве параметра. Класс Filelnfo имеет конструктор, который создает объект, представляющий файл. Затем используются методы для обработки этого определенного файла.

Методы класса File (Файл) всегда выполняют проверку защиты. Если необходимо непрерывно обращаться к определенному файлу, можно использовать класс Filelnfo, так как в этом классе проверка защиты производится только однажды — в конструкторе. Защита обсуждается более подробно в главе 13 "Защита".

Класс File (Файл)

Класс File (Файл) содержит методы для создания и открытия файлов, которые возвращают объекты FileStream, StreamWriter или StreamReader, производящие фактическое чтение и запись. Перегруженный метод Create (Создать) возвращает объект FileStream. Метод CreateText возвращает StreamWriter. Перегруженный метод Open (Открыть) в зависимости от передаваемых параметров может создавать новый файл или открывать существующий для чтения или записи. Возвращаемый объект — объект FileStream. Метод OpenText возвращает StreamReader. Метод OpenRead возвращает объект FileStream. Метод OpenWrite возвращает объект типа FileStream.

Класс File (Файл) содержит также методы копирования, удаления и перемещения файлов. К тому же, мы можем проверить существование файла. Нижеперечисленные атрибуты файла можно прочитать и изменить:

  • время создания;
  • время последнего обращения;
  • последнее время записи;
  • архивный, скрытый, обычный, системный или временный;
  • сжатый, зашифрованный;
  • только для чтения;
  • файл — это каталог?
  • Класс Path (Путь)

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

Статические методы этого класса позволяют изменять расширение файла или находить папку с временными файлами. Особенно полезен метод GetFullPath. Ему можно передать относительный путь, например \foo.txt, и он возвратит полный путь файла. Это очень полезно для класса File (Файл) или классов защиты, для которых требуется указывать полный путь к файлу.

Класс Filelnfo

Конструктор Filelnfo создает объект, который представляет дисковый файл. Конструктор принимает один параметр — строку, содержащую имя файла. Объект будет иметь свойства, отражающие свойства файла, такие как время создания, размер и полный путь к файлу. Класс содержит методы для создания и открытия файла, которые аналогичны методам класса File (Файл), но работают с конкретным экземпляром файла и поэтому не нуждаются в таком параметре, как имя файла. Класс Filelnf о также содержит методы, позволяющие перемещать и копировать файлы.

Пример File (Файл)

Пример File (Файл) в папке FilelO иллюстрирует использование классов Filelnfo и File (Файл). В этом примере используется статический метод Delete (Удалить) класса File (Файл) для удаления указанного файла. Затем статический метод CreateText создает новый файл и возвращает экземпляр StreamWriter, который используется для записи текста в файл. Далее поток закрывается, и статический метод Move (Переслать) переименовывает файл. Потом создается экземпляр Filelnfo, который будет представлять этот переименованный файл. Полное имя файла, размер и дата его создания выводятся на консоль. Файл открывается как текстовый, после чего используется экземпляр streamReader, чтобы прочитать и вывести на консоль содержимое файла.

File::Delete("file2.txt"); // Удалить файл "file2.txt"
StreamWriter *sw =
System::IO::File::CreateText("file.txt");
sw->Write ("The time has come the Walrus said, "); // Поговорить...
sw->WriteLine("to talk of many things.");
sw->Write("Of shoes, and ships, and sealing wax, "); // о ботинках, и судах, и сургуче
sw->WriteLine("of cabbages and kings."); // капусте и о королях
sw->Write("And why the sea is boiling hot, "); // И почему
// море кипит
sw->WriteLine("and whether pigs have wings."); // и имеют ли
// свиньи крылья.
sw->Close();
File::Move("file.txt", "file2.txt");
//Файл:: Переслать ("file.txt", "file2.txt");
Filelnfo *filelnfo = new Filelnfo("file2.txt");
Console::WriteLine(
"File {0} is {1} bytes in length, created on {2}",
// "Файл {0} - {1} байтов, создан {2} ",
file!nfo->FullName,
_box(file!nfo->Length), // Длина
_box(file!nfo->CreationTime));
Console::WriteLine("");
StreamReader *sr = file!nfo->OpenText();
String *s = sr->ReadLine();
// Строка
while (s != 0)
// пока (s != 0)
{
Console::WriteLine(s);
s = sr->ReadLine();
}
sr->Close();
Console::WriteLine("");

Сохранение сложной структуры данных со связанными объектами может стать довольно громоздким при использовании классов File (Файл) и Stream (Поток, Абстрактный последовательный файл). Необходимо сохранить все индивидуальные поля на диске. При этом нужно помнить, какое из полей какому объекту принадлежит, и какой экземпляр класса связан с другими экземплярами объектов. При восстановлении нужно воссоздать порядок расположения полей и объектные ссылки.

Каркас .NET Framework предоставляет технологию сериализации (преобразования в последовательную форму), которая выполняет все вышесказанное самостоятельно. Сериализация преобразовывает объекты, такие как классы, структуры и массивы в поток байтов. При преобразовании из последовательной формы в параллельную поток байтов преобразовывается обратно в объекты. Сериализация и преобразование из последовательной формы в параллельную могут быть проделаны на различных машинах, если на них работает общеязыковая среда времени выполнения CLR.

Объекты могут быть преобразованы в последовательную форму без применения специально написанного кода, потому что, как мы видели, во время выполнения можно запросить метаданные объекта, что дает возможность узнать распределение памяти, занятой этим объектом. Чтобы информировать каркас, что класс может быть преобразован в последовательную форму, нужно пометить класс атрибутом System: :Serializable (Система::Преобразуемый в последовательную форму). Любое поле или свойство, которые не должны быть преобразованы в последовательную форму, могут быть отмечены атрибутом System: :NonSerialized (Система::Непреобразуемый в последовательную форму). Например, поля, которые представляют собой кэшированные переменные, не должны преобразовываться в последовательную форму. Все, что нужно сделать — пометить класс атрибутом, указывающим, что класс может быть преобразован в последовательную форму. И тогда нет необходимости в любом другом коде, выполняющем преобразование в последовательную форму.

Пример Serialization (Сериализация) показывает, как применить преобразование в последовательную форму для изучения класса HotelBroker в сборке Hotel (Гостиница). Атрибут Serializable (Преобразуемый в последовательную форму) применяется в определении класса HotelBroker. Атрибут сериализации Serializable (Преобразуемый в последовательную форму) применяется также ко всем классам, которые используются классом HotelBroker или производными от базовых классов класса HotelBroker: Broker (Брокер), Hotel (Гостиница), HotelReservation, Reservable и Reservation (Резервирование), потому что при сериализации HotelBroker должны быть также преобразованы в последовательную форму и эти классы. Если бы любой из этих классов не был отмечен атрибутом, то во время выполнения могло бы произойти исключение при попытке сериализации объекта этого типа.

[Serializable]
// [Преобразуемый в последовательную форму]
public _gc class HotelBroker:
// класс сборщика мусора HotelBroker:
public Broker,
// общедоступный Брокер
public IHotellnfo,
public IHotelAdmin,
public IHotelReservation
{
private: // частный
const int MAXDAY; // константа
const int MAXUNIT; // константа
[NonSerialized] ArrayList *cities;
// [Непреобразуемый в последовательную форму]
};
[Serializable]
// [Преобразуемый в последовательную форму]
public _gc class Hotel :
public Reservable
// класс сборщика мусора Гостиница:
Reservable
{
};
[Serializable]
// [Преобразуемый в последовательную форму]
public _gc class HotelReservation :
public Reservation
// общедоступный класс сборщика мусора HotelReservation:
Резервирование
{
} ;
[Serializable]
// [Преобразуемый в последовательную форму]
public _gc _abstract class Reservable
// сборщик мусора - абстрактный класс Reservable
{
} ;
[Serializable]
// [Преобразуемый в последовательную форму]
public _gc _abstract class Reservation
// сборщик мусора - абстрактный класс Reservation
{
} ;
[Serializable]
// [Преобразуемый в последовательную форму]
public _gc _abstract class Broker
// сборщик мусора - абстрактный класс Broker
{
};

Поле cities (города) было помечено как NonSerialized (Непреобразуемый в последовательную форму), так как название города, где размещается гостиница, сохраняется вместе с преобразованными в последовательную форму названиями гостиниц. Поэтому оно может быть восстановлено с помощью модифицированного метода AddCity. Поле cities (города) было бы пусто после преобразования класса HotelBroker из последовательной формы в параллельную, потому что оно не было сохранено.

private: // частный
void AddCity(String *city) // (Строка *city)
{
if (cities == 0) // если (города == 0)
{
cities = new ArrayList; // города
lEnumerator *pEnum = units->GetEnumerator();
while (pEnum->MoveNext() )
{
Hotel *h = // Гостиница
dynamic_cast(pEnum->Current);
AddCity(h->City); // Город
}
}
// check if city already on list, add if not
// проверить, есть ли город уже в списке, добавить если нет
if (!cities->Contains(city))
// если (! города-> Содержат (город))
cities->Add(city);
// города-> Добавить (город);
}

Хотя каркас знает, как сохранять объект, помеченный атрибутом Serializable (Преобразуемый в последовательную форму), но все же необходимо определить формат, в котором будет сохранен объект, и носитель данных. Чтобы определить формат, в котором будет сохранен объект, нужно использовать экземпляр объекта, который поддерживает интерфейс IFormatter.

Каркас имеет два таких класса: System: :Runtime: :Serialization: : Formatters :: Binary: : BinaryFormatter (Система::Время выполнения:: Преобразование в последовательную форму::Форматеры::Двоичный::ВтагуРогтаиег) и System :: Runtime :: Serialization :: Formatters :: Soap :: Soар Formatter (Система :: Время выполнения :: Преобразование в последовательную форму :: Форматеры :: Sоар:: Soap-Formatter). BinaryFormatter использует двоичный, компактный формат для сериализации и преобразования из последовательной формы в параллельную на платформах, которые поддерживают общеязыковую среду времени выполнения CLR. SoapForrnatter использует промышленный стандарт протокола SOAP, который обсуждается в главе 11 "Web-службы". Так как он основан на XML, и, следовательно, является текстовым протоколом, он может использоваться для связи с платформой, не основанной на общеязыковой среде времени выполнения CLR. Двоичный формат быстрее при сериализации и преобразовании данных из последовательной формы в параллельную.

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

Пример Serialization (Сериализация) содержит код, демонстрирующий использование FileStream для сохранения и восстановления обоих форматов: двоичного и SOAP. Конечно, можно использовать любой класс, производный от Stream (Поток, Абстрактный последовательный файл), лишь бы он представлял некоторый носитель данных. Необходимо предпринять специальные меры, чтобы гарантировать, что метод Load (Загрузка) сможет изменять параметр, который указывает на HotelBroker. Для этого параметр объявляется как ссылка на указатель, указывающий на HotelBroker.

static void Save( // статический метод Сохранить
HotelBroker *broker, String *formatter)
{
FileStream *s;
if (String::Equals(formatter, "b"))
// если (Строка::Равняется (форматер, "b"))
{
s = new FileStream(
"hotels.bin", FileMode::Create); // Создать
BinaryFormatter *b = new BinaryFormatter;
b->Serialize (s, broker);
// Преобразовать в последовательную форму (s, брокер);
}
else
{
s = new FileStream(
"hotels.txt", FileMode::Create); // Создать
SoapFormatter *sf = new SoapFormatter;
sf->Serialize(s, broker);
// Преобразовать в последовательную форму (з, брокер);
}
s->Close ();
}
static void Load( // статический метод Загрузка
HotelBroker *&broker, /* ссылка на указатель */
String *formatter) // Строка
{
FileStream *s;
if (String::Equals(formatter, "b"))
// если (Строка::Равняется (форматер, "b"))
{
s = new FileStream("hotels.bin", FileMode::Open); // Открыть
BinaryFormatter *b = new BinaryFormatter; broker = // брокер
dynamic_cast
(b->Deserialize (s) ) ;
}
else
{
s = new FileStream("hotels.txt", FileMode::Open);
// Открыть
SoapFormatter *sf = new SoapFormatter;
broker = // брокер
dynamic_cast(sf->Deserialize(s));
}
s->Close();
ShowHotelList(broker->GetHotels());
}

Ниже приведен некоторый типовой вывод примера Serialization (Сериализация): сначала мы добавляем название гостиницы и сохраняем его с помощью форматера SOAP. Затем мы выходим из программы.

Введите команду: города
Атланта
Бостон
Команды: выход, города, список, добавить, выборка, сохранить
Введите команду: список
Город Название Номера Цена
Атланта Дикси 100 115
Атланта Мэриот 500 70
Бостон Шератон 250 95
Команды: выход, города, список, добавить, выборка, сохранить
Введите команду: добавить
Город Гостиницы: Филадельфия
Название Гостиницы: Фрэнклин
Номера: 100
Цена: 200
Команды: выход, города, список, добавить, выборка, сохранить
Введите команду: сохранить
Форматер: b(inary), s(oap)s
Команды: выход, города, список, добавить, выборка, сохранить
Введите команду: города
Атланта
Бостон
Филадельфия
Команды: выход, города, список, добавить, выборка, сохранить
Введите команду: список
Город Название Номера Цена
Атланта Дикси 100 115
Атланта Мзриот 500 70
Бостон Шератон 250 95
Филадельфия Фрэнклин 100 200
Команды: выход, города, список, добавить, выборка, сохранить
Введите команду: выход

Затем выполняем программу снова и восстанавливаем данные, сохраненныепри первом запуске.

Введите команду: города
Атланта
Бостон
Команды: выход, города, список, добавить, выборка, сохранить
Введите команду: список
Город Название Номера Цена
Атланта Дикси 100 115
Атланта Мэриот 500 70
Бостон Шератон 250 95
Команды: выход, города, список, добавить, выборка, сохранить
Введите команду: выборка
Форматер: b(inary), s(oap)s
Город Название Номера Цена
Атланта Дикси 100 115
Атланта Мэриот 500 70
Бостон Шератон 250 95
Филадельфия Фрэнклин 100 200
Команды: выход, города, список, добавить, выборка, сохранить
Введите команду: города
Атланта
Бостон
Филадельфия

Иногда методов преобразования в последовательную форму (сериализации), предоставляемых каркасом, недостаточно. В таком случае можно предусмотреть специальную сериализацию класса. Для этого необходимо реализовать интерфейс ISerializable и добавить к классу конструктор, как это показано в проекте Serialization (Сериализация) в папке ISerializable. Интерфейс ISerializable имеет один член: GetObj ectData. Этот метод используется во время сериализации данных.

Пример ISerializable демонстрирует, как это можно сделать. Как и прежде, класс должен быть отмечен как Serializable (Преобразуемый в последовательную форму).

[Serializable]
// [Преобразуемый в последовательную форму]
public _gc class HotelBroker :
// класс сборщика мусора HotelBroker:
public Broker, // общедоступный Брокер
public iHotellnfo,
public IHotelAdmin,
public IHotelReservation,
public ISerializable
{
};

Чтобы сохранить все данные, которые должны быть сохранены в методе ISerializable: : GetObjectData, используется класс Serializationlnfo. Для управления сохранением различных типов данных, включая Object*, перегружается метод AddValue класса Serializationlnfo. Когда сохраняется тип, необходимо назначить ему имя так, чтобы можно было позже его восстановить. Класс StreamingContext предоставляет информацию о потоке, используемом при сериализации. Например, можно узнать, является ли используемый поток файлом или удаленным потоком, который посылается на другой компьютер.

public:
void GetObjectData(Serializationlnfo *info,
StreamingContext context) // контекст
{
long numberHotels = units->Count;
info->AddValue("NumberHotels", numberHotels) ;
info->AddValue("Hotels", units); // Гостиницы
}

Нужно также реализовать специальный конструктор, который используется каркасом, когда объект преобразуется из последовательной формы в параллельную. Он принимает те же параметры, что и GetObjectData. В нем для восстановления данных используются различные методы GetXXX класса Serializationlnfo. Обращаем внимание читателя на то, что, поскольку мы не сохраняли поле cities (города), то были вынуждены восстанавливать его вручную. Конструктор объявляется приватным, потому что использует его только каркас. Если забыть добавить конструктор, то при попытке восстановить объект будет запущено исключение SerializationException.

private: // частный
HotelBroker(Serializationlnfo *info,
StreamingContext context) // контекст
: Broker(366, 10), MAXDAY(366), MAXUNIT(IO) // Брокер
{
long numberHotels =
info->Get!nt32("NumberHotels");
units = dynamic_cast(
info->GetValue(
"Hotels", units->GetType())); // Гостиницы
if (numberHotels == units->Count)
Console::WriteLine("All hotels deserialized.");
// "Все гостиницы преобразованы из последовательной
// формы в параллельную. "
else
Console::WriteLine("Error in deserialization.");
// "Ошибка при преобразовании из последовательной
// формы в параллельную."
cities = new ArrayList;
// города
lEnumerator *pEnum = units->GetEnumerator();
while (pEnum->MoveNext())
{
Hotel *h = // Гостиница
dynamic_cast(pEnum->Current); // Гостиница
AddCity(h->City); // Город
}
}

Необходимо помнить, что после внесения любых изменений и последующей компоновки проекта Hotel (Гостиница), нужно скопировать Hotel. dll в папку, где находится программа-клиент Serialization.exe. Это не происходит, как в С#, автоматически, если не добавить в проект специальный шаг компоновки.

В данном примере мы выполняли пользовательскую сериализацию только объекта HotelBroker. Для всех других объектов мы использовали сериализацию, предоставляемую каркасом. Этот пример работает так же, как и пример Serialization (Сериализация). Поэтому вывод программы выглядит аналогично.

Сериализация дала конкретный пример гибкости среды каркаса .NET Framework, используемой при написании кода. Теперь давайте рассмотрим модель, в которой выполняются .NET-приложения. Среда платформы Win32, в которой выполняется программа, называется ее процессом. Эта среда состоит из

  • адресного пространства, в котором хранится код и данные программы;
  • набора переменных среды, связанных с программой;
  • текущего диска и папки;
  • одного или более потоков.

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

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

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

Класс System: : Threading:: Thread (Система::Организация поточной обработ-ки::Поток) моделирует выполняющийся поток. Объект Thread (Поток), который представляет текущий выполняющийся поток, может быть найден с помощью статического свойства Thread :: CurrentThread.

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

Чтобы помочь читателю лучше разобраться в работе потоков, мы предоставим многошаговый пример Threading (Организация поточной обработки), в котором используются сборки Customer (Клиент) и Hotel (Гостиница). Сначала мы ознакомимся с программой, а затем выполним резервирование. Итак, для начала рассмотрим шаг 0.

Потоки .NET выполняются как делегаты, определенные классом ThreadStart. Делегат ничего не возвращает (void) и не имеет никаких параметров.

public _gc _delegate void ThreadStart();
// сборщик мусора - делегат ThreadStart ();

Класс NewReservation имеет общедоступный (public) метод MakeReservation, который определяет функцию потока. Так как функция потока не принимает никаких параметров, то в качестве данных она может использовать только поля экземпляра NewReservation.

Делегат потока создается и передается в качестве параметра для конструктора, который создает объект Thread (Поток). Метод Start (Пуск) объекта Thread (Поток) вызывается для того, чтобы начать выполнение потока. Когда мы будем рассматривать модель программирования асинхронных событий, мы покажем, как передать параметры делегату потока. Программа теперь имеет два потока: первоначальный, который выполнял код запуска потока, и поток, который мы только что создали и который попытается забронировать места в гостинице.

public _gc class NewReservation
// класс сборщика мусора NewReservation
{
public:
void MakeReservation()
{
Console::WriteLine(
"Thread {0} starting.", // Запускается поток {О}.
Thread::CurrentThread-> // Поток
GetHashCode().ToString());
ReservationResult *result =
hotelBroker->MakeReservation(
customerld, city, hotel, date, numberDays);
// customerld, город, гостиница, дата, numberDays
}
};

Затем в методе Main (Главный) следующий код запускает поток, используя при этом делегат:

NewReservation *reservel =
new NewReservation(customers, hotelBroker); // клиенты
reservel->customerld = 1;
reservel->city = "Boston"; // город = "Бостон";
reservel->hotel = "Presidential"; // гостиница = "Президентская";
reservel->sdate = "12/12/2001";
reservel->numberDays = 3;
// создать делегат для потоков
ThreadStart *threadStartl = new ThreadStart(
reservel,
reservel->MakeReservation);
Thread *threadl = new Thread(threadStartl); // новый Поток
Console::WriteLine(
"Thread {0} starting a new thread.",
// "Поток {0} запускает новый поток. "
Thread::CurrentThread-> // Поток
GetHashCode().ToString());
threadl->Start ();

Чтобы заставить первоначальный поток ожидать завершения работы второго потока, вызывается метод Join (Объединить) объекта Thread (Поток). Первоначальный поток теперь заблокирован (находится в состоянии ожидания), пока не завершит работу поток, резервирующий место в гостинице. Результат запроса резервирования места выводится на консоль резервирующим потоком.

// Блокировать этот поток, пока не завершится рабочий поток
threadl->Join(); // Объединение
Console::WriteLine("Done!"); // Завершен

Синхронизация потоков

Приложение может создать несколько потоков. Сейчас мы рассмотрим код на шаге 1 из примера Threading (Организация поточной обработки). Теперь несколько запросов о резервировании делаются одновременно.

NewReservation *reservel =
new NewReservation(customers, hotelBroker); // клиенты
NewReservation *reserve2 =
new NewReservation(customers, hotelBroker); // клиенты
// создать делегат для потоков
ThreadStart *threadStartl = new ThreadStart(
reservel,
reservel->MakeReservation);
ThreadStart *threadStart2 = new ThreadStart(
reserve2,
reserve2->MakeReservation);
Thread *threadl = new Thread(threadStartl); // новый Поток
Thread *thread2 = new Thread(threadStart2); // новый Поток
Console::WriteLine(
"Thread {0} starting a new thread.",
// "Поток {0} запустил новый поток. "
Thread::CurrentThread-> // Поток
GetHashCode().ToString());
threadl->Start(); // Пуск
thread2->Start();// Пуск
// Блокировать этот поток, пока не завершится рабочий поток
threadl->Join(); // Объединение
thread2->Join(); // Объединение

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

Рассмотрим несколько участков в коде, где происходит заказ места в гостинице и там, где происходит резервирование свободных мест — именно в этих точках могут возникнуть трудности подобного рода. Исследуем код для Broker: : Reserve (Брокер::Резерв) в Broker. h. Сначала сделаем проверку существующих заказов для данной гостиницы и для данной даты, чтобы узнать, есть ли свободные номера. Тогда, если есть в наличии свободный номер, он резервируется. Мы добавили вызов метода Thread: :Sleep (Поток-Режим ожидания) между кодом, который проверяет имеющиеся в распоряжении номера и кодом, который резервирует свободный номер. Вскоре будет объяснено, для чего это было сделано.

// Проверить, есть ли номера для всех дат
for (int i = day; i < day + numDays; i++)
{
if (numCust[i, unitid] >= unit->capacity)
{
result->Reservation!d = -1; // результат
result->Comment = "Room not available";
// результат-> Комментарий = "Номера не доступны";
return result; // результат
}
}
Console::WriteLine(
"Thread {0} finds the room is available in
Broker::Reserve",
// "Поток {0} находит, что номер доступен в
// Брокер:: Резерв ",
Thread::CurrentThread-> // Поток GetHashCode{).ToString());
Thread::Sleep(0); // Поток:: Бездействие
// Резервировать номер для требуемых дат
for (int i = day; i < day + numDays; i++)
numCust[i, unitid] += 1;

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

Чтобы смоделировать возникновение такой ситуации, на этом шаге (шаг 1) примера Threading (Организация поточной обработки) поместим вызов метода Thread:: Sleep (Поток::Режим ожидания) между кодом, который проверяет имеющиеся в распоряжении номера и кодом, который резервирует свободный номер. Вызов Sleep (0) заставляет поток прекратить выполнение и уступить остаток своего временного отрезка.

Чтобы удостовериться в том, что мы наблюдаем именно возникновение проблемы соперничества между потоками, мы имеем в своем распоряжении всего один номер в целой гостинице! Затем мы подготавливаем нашу программу так, чтобы два потока пробовали резервировать единственный номер в гостинице на ту же самую дату. Рассмотрим код в подпрограмме Main (Главная), который все это подготавливает:

hotelBroker->AddHotel(
"Boston", // "Бостон",
"Presidential", // "Президентская",
1, // только один номер во всей гостинице!
(Decimal) 10000); // (Десятичное число)
NewReservation *reservel =
new NewReservation(customers, hotelBroker); // клиенты
reservel->customerld = 1;
reservel->city = "Boston"; // город = "Бостон";
reservel->hotel = "Presidential"; // гостиница = "Президентская";
reservel->sdate = "12/12/2001";
reservel->numberDays = 3;
NewReservation *reserve2 =
new NewReservation(customers, hotelBroker); // клиенты
reserve2->customerld = 2;
reserve2->city = "Boston"; // город = "Бостон";
reserve2->hotel = "Presidential"; // гостиница = "Президентская";
reserve2->sdate = "12/13/2001";
reserve2->numberDays = 1;

Выполнение программы даст приведенный ниже результат:

Added Boston Presidential Hotel with one room.
Thread 3 starting a new thread.
Thread 5 starting.
Thread 6 starting.
Reserving for Customer 2 at the Boston Presidential Hotel on
12/13/2001 12:00:00 AM for 1 days
Reserving for Customer 1 at the Boston Presidential Hotel on
12/12/2001 12:00:00 AM for 3 days
Thread 6 entered Broker::Reserve
Thread 6 finds the room is available in Broker::Reserve
Thread 5 entered Broker::Reserve
Thread 5 finds the room is available in Broker::Reserve
Thread 6 left Broker::Reserve
Reservation for Customer 2 has been booked
Reservationld = 1
Thread 5 left Broker::Reserve
Reservation for Customer 1 has been booked
Reservationld = 2
ReservationRate = 10000
ReservationCost = 30000
Comment = OK
ReservationRate = 10000
ReservationCost = 10000
Comment = OK
Done!

Перевод такой:

Добавлена Бостонская Президентская гостиница с одним номером.
Поток 3 стартовал новый поток.
Поток 5 стартовал.
Поток 6 стартовал.
Резервирование для Клиента 2 в Бостонской Президентской гостинице на
12/13/2001 12:00:00 AM в течение 1 дня
Резервирование для Клиента 1 в Бостонской Президентской гостинице на
12/12/2001 12:00:00 AM в течение 3 дней
Поток 6 ввел Брокер:: Резерв
Поток 6 находит, что номер доступен в Брокер::Резерв
Поток 5 ввел Брокер::Резерв
Поток 5 находит, что номер доступен в Брокер::Резерв
Поток 6 выходит из Брокер::Резерв
Резервирование для Клиента 2 было заказано
Reservationld = 1
Поток 5 выходит из Брокер::Резерв
Резервирование для Клиента 1 было заказано
Reservationld = 2
ReservationRate = 10000
ReservationCost = 30000
Комментарий = OK
ReservationRate = 10000
ReservationCost = 10000
Комментарий = OK
Сделано!

К сожалению, оба клиента добиваются резервирования последнего (единственного) номера на 13 декабря! Обратите внимание на то, как один из потоков выполняет метод Reserve (Резерв) и находит, что номер доступен прежде, чем прекратит свою работу. Затем другой поток выполняет Reserve (Резерв) и также обнаруживает свободный номер прежде, чем закончится отведенный для него промежуток времени. Потом оба потока заказывают тот же самый номер гостиницы.

Операционные системы предоставляют средства синхронизации взаимодействия нескольких потоков и процессов, получающих доступ к совместно используемым ресурсам. Каркас .NET Framework предусматривает несколько механизмов предотвращения конфликтов между потоками.

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

Синхронизация с помощью мониторов

Класс System: :Threading: :Monitor (Система::Организация поточной об-работки::Монитор) позволяет потокам синхронизировать доступ к объектам, чтобы избежать нарушения целостности данных и неопределенного поведения вследствие соперничества между потоками. Шаг 2 из примера Threading (Организация поточной обработки) демонстрирует использование класса Monitor (Монитор) с указателем this экземпляра HotelBroker.

ReservationResult *Reserve(Reservation *res) // Резервирование
{
Console::WriteLine(
"Thread {0} trying to enter Broker::Reserve",
// "Поток {0} пытается войти в Брокер::Резерв",
Thread::CurrentThread-> // Поток
GetHashCode() .ToString ()); Monitor::Enter(this); // Монитор::Войти
. . . (спорный код потока здесь) Monitor::Exit(this); // Выход
return result; // результат
}

Потоку, который первым вызовет метод Monitor: : Enter (this), будет разрешено выполнить метод Reserve (Резерв), потому что он овладеет замком метода Monitor (Монитор) с помощью указателя this. Потоки, пробующие выполнить те же действия, должны будут ожидать, пока первый поток не разблокирует замок с помощью метода Monitor: :Exit (this). После этого и другие потоки смогут вызвать Monitor::Enter(this) и овладеть замком.

Поток может вызывать Monitor: :Enter (Монитор::Войти) несколько раз, но каждый вызов должен быть скомпенсирован вызовом Monitor: :Exit (Монитор::Выйти). Если поток пытается овладеть замком, но не хочет заблокировать себя, то он может использовать метол Monitor::TryEnter.

Теперь, когда мы обеспечили синхронизацию, случай, идентичный испытанному в шаге 1, не приведет к повторному резервированию одного и того же гостиничного номера на одно и то же время. Обращаем внимание читателя на то, сколько времени второй поток не может начать выполнение метода Reserve (Резерв) до тех пор, пока первый поток, который начал выполнение этого метода раньше, не покинет критическую секцию. Это решает нашу проблему конкуренции между потоками и устанавливает правило, согласно которому один и тот же номер гостиницы не может быть забронирован двумя клиентами на ту же самую дату.

Added Boston Presidential Hotel with one room.
Thread 3 starting a new thread.
Thread 5 starting.
Thread б starting.
Reserving for Customer 2 at the Boston Presidential Hotel on
12/13/2001 12:00:00 AM for I days
Thread 6 trying to enter Broker::Reserve
Thread 6 entered Broker::Reserve
Thread 6 finds the room is available in Broker::Reserve
Thread 6 left Broker::Reserve
Reservation for Customer 2 has been booked
Reservationld = 1
Reserving for Customer 1 at the Boston Presidential Hotel on
12/12/2001 12:00:00 AM for 3 days
Thread 5 trying to enter Broker::Reserve
Thread 5 entered Broker::Reserve
Reservation for Customer 1 could not be booked
Room not available
ReservationRate = 10000
ReservationCost = 10000
Comment = OK
Done !

Перевод такой:

Добавлена Бостонская Президентская гостиница с одним номером.
Поток 3 стартовал новый поток.
Поток 5 стартовал.
Поток 6 стартовал.
Резервирование для Клиента 2 в Бостонской Президентской гостинице на
12/13/2001 12:00:00 AM в течение 1 дня
Поток 6 пытается войти в Брокер::Резерв
Поток 6 вошел в Брокер::Резерв
Поток 6 находит, что номер доступен в Брокер::Резерв
Поток 6 вышел из Брокер::Резерв
Резервирование для Клиента 2 было заказано
Reservationld = 1
Резервирование для Клиента 1 в Бостонской Президентской гостинице на
12/12/2001 12:00:00 AM в течение 3 дней
Поток 5 пытается войти в Брокер::Резерв
Поток 5 вошел в Брокер::Резерв
Резервирование для Клиента 1 не могло быть заказано
Номера не доступны
ReservationRate = 10000
ReservationCost = 10000
Комментарий = OK
Сделано!

Уведомление с помощью мониторов

s
First thread: 2 started.
Thread: 5 started.
Thread: 5 waiting.
Thread: 6 started.
Thread: 6 waiting.
Thread 5 sleeping.
Done .
Thread 5 awake.
Thread: 5 exited.
Thread 6 sleeping.
Thread 6 awake.
Thread: 6 exited.

Перевод такой:

Первый поток: 2 стартовал.
Поток: 5 стартовал.
Поток: 5 в состоянии ожидания.
Поток: 6 стартовал.
Поток: 6 в состоянии ожидания.
Поток 5 бездействует.
Сделано.
Поток 5 активный.
Поток: 5 вышел.
Поток 6 бездействует.
Поток 6 активный.
Поток: 6 вышел.

Класс X содержит поле "о" типа Ob j ect (Объект), которое будет использоваться в качестве синхронизирующего замка.

Класс также содержит метод Test (Испытание), который используется в качестве делегата потока. Метод овладевает синхронизирующим замком, а затем ждет уведомления. Когда он получит уведомление, то бездействует в течение половины секунды, а затем вновь освобождает замок.

Метод main (главный) создает два потока, которые используют метод X: :Test (Х::Испытание) в качестве делегата своего потока и совместно используют тот же самый объект, предназначенный для синхронизации. Затем он бездействует в течение 2 секунд, чтобы позволить потокам произвести запросы ожидания и освободить замки. Потом метод main (главный) вызывает метод PulseAll, чтобы уведомить оба ожидающих потока и освободить замки. В конечном счете, каждый поток повторно овладевает замком, выводит сообщение на консоль, и в последний раз освобождает замок.

_gc class X
// класс сборщика мусора X
{ private: // частный
Object *o; // Объект public:
X(Object *o) // Объект
{
this->o = о;
}
void Test () // Испытание
{
try
{
long threadld =
"Thread->GetHashCode () ; // Поток
console::WriteLine(
"Thread: {0} sta.ted.",
threadld.ToString()); // "Поток: {0} стартовал."
Monitor::Enter(о1, // Монитор::Войти
Console : : Write1" jre (
'"In.-'ad: tO) waiting.",
threadld.ToString()); // "Поток: {0} ожидает."
Monitor::Wait(о); // Монитор::Ждет
Console::WriteLine( "Thread {0} sleeping.",
threadld.ToString()); // "Поток {0} бездействует."
Thread::Sleep(500); // Поток::Бездействовать
Console::WriteLine( "Thread {0} awake.",
threadld.ToString()); // "Поток {0} активный. "
Monitor::Exit (о); // Выход
Console::WriteLine( "Thread: {0} exited.",
threadld.ToString()); // "Поток: {0} вышел."
}
catch(Exception *e) // Исключение
{
long threadld =
Thread::CurrentThread->GetHashCode();
Console::WriteLine(
"Thread: {0} Exception: {!}", // "Поток: {0} Исключение: {1} "
threadld.ToString(), e->Message); // Сообщение Monitor::Exit(о); // Выход
}
}
};
_gc class Classl
// класс сборщика мусора Classl
{
public:
static Object *o = new Object;
// статический Объект *о = новый Объект;
static void Main()
{
Console::WriteLine(
"First thread: {0} started.",
// "Первый поток: {0} стартовал. ",
Thread::CurrentThread->GetHashCode().ToString() ) ;
X *a = new X(o); X *b = new X(o);
ThreadStart *ats = new ThreadStart(a, X::Test); // Испытание
ThreadStart *bts = new ThreadStart(b, X::Test); // Испытание
Thread *at = new Thread(ats); // новый Поток
Thread *bt = new Thread(bts); // новый Поток
at->Start(); // Начало bt->Start (); // Начало
// Бездействовать, чтобы позволить другим потокам ждать
// объект перед Импульсом (Pulse) Thread::Sleep(2000);
// Бездействие Monitor::Enter (о);
// Монитор::Войти
Monitor::PulseAll(о);
//Monitor::Pulse(о);
Monitor::Exit(о);
// Выход
Console::WriteLine("Done.");
// Сделано
}
};

Только один поток сможет завершить свою работу, если закомментировать вызов PulseAll и убрать комментарий с вызова метода Pulse (Импульс, Сигнал), потому что другие потоки никогда не смогут стать в очередь готовых потоков. Если удалить Sleep (2000) из главной подпрограммы main (главная), то другие потоки заблокируются навсегда, потому что посылка сигнала происходит до того, как потоки получат шанс вызвать метод Wait (Ожидать), и, следовательно, они никогда не получат уведомления. Методы Wait (Ожидать), Pulse (Импульс, Сигнал) и PulseAll могут использоваться для координирования использования несколькими потоками синхронизационных замков.

Метод Thread: : Sleep (Поток::Режим ожидания) приводит к приостановке выполнения текущего потока на указанный период времени. Вызов Thread: : Suspend (Поток::Приостановить) блокирует выполнение потока до вызова Thread: :Resume (Поток::Продолжить) другим потоком. Поток также может быть заблокирован, если он ожидает завершения другого потока (Thread: : Join (Поток::Объединить)). Этот метод использовался в примерах Threading (Организация поточной обработки) так, чтобы главный поток мог ждать завершения выполнения запросов резервирования. Поток может также блокироваться при ожидании синхронизирующего замка (в критической секции).

Вызов Thread: : Interrupt (Поток::Прерывание) для заблокированного потока приводит к его пробуждению. Поток получит ThreadlnterruptedException. И если он не перехватывает это исключение, то среда времени выполнения это исключение перехватит и уничтожит поток.

Если, в качестве последней надежды, нужно уничтожить поток напрямую, необходимо для этого потока сделать вызов метода Thread: :Abort (Поток:прекратить). Метод Thread: :Abort (Поток::Прекратить) приводит к запуску исключения Thread-AbortException. Это исключение не может быть перехвачено, но оно приведет к выполнению всех блоков finally (наконец). Кроме того, Thread: :Abort (Поток:: Прекратить) не приводит к пробуждению ожидающего потока.

Поскольку для выполнения блоков finally (наконец) может потребоваться время, или потоки могут находиться в состоянии ожидания, то потоки, выполнение которых прекращается, могут быть завершены не сразу. Поэтому, если нужно убедиться в том, что выполнение потока завершено, необходимо ожидать завершения потока, вызвав метод Thread: : Join (Поток::Объединить).

Классы синхронизации

Каркас .NET Framework содержит классы, представляющие собой стандартные в Win32 объекты синхронизации. Все эти классы являются производными от абстрактного класса WaitHandle. Этот класс имеет статические методы Wait All и WaitAny, которые позволяют ожидать сигнал от нескольких (или только от одного) объектов синхронизации. Он также содержит метод экземпляра WaitOne, который позволяет ожидать сигнал отданного экземпляра. Способ приема сигнала объектом зависит от конкретного типа объекта синхронизации, который является производным от WaitHandle.

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

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

Автоматическая синхронизация

Атрибуты можно использовать для синхронизации доступа к методам экземпляра (нестатическим) и нестатическим полям класса. Доступ к статическим полям и методам не синхронизируется. Чтобы использовать эту возможность нужно создать класс, производный от класса System: :ContextBoundObject и применить к нему атрибут Synchronization (Синхронизация). Этот атрибут не применяется к отдельному полю или методу.

Данный атрибут находится в пространстве имен System :: Runtime :: Remoting :: Contexts (Система::Время выполнения:: Remoting::Контексты). Он описывает синхронизационные требования для экземпляра класса, к которому применяется. В конструктор SynchronizationAttribute можно передать одно из четырех значений, являющихся статическими полями класса SynchronizationAttribute: NONSUPPORTED, SUPPORTED (ПОДДЕРЖИВАЕМЫЙ), REQUIRED (ТРЕБУЕМЫЙ), REQUIRES_NEW. Пример Threading (Организация поточной обработки) на шаге 3 иллюстрирует, как это сделать.

using namespace System :: Runtime :: Remoting :: Contexts;
// использование пространства имен
// Система :: Время выполнения :: Remoting :: Контексты;
// SynchronizationAttribute :: REQUIRED (ТРЕБУЕМЫЙ) - 4
[Synchronization(4) ]
// [Синхронизация (4)]
public _gc _abstract class Broker :
// сборщик мусора - абстрактный класс Broker:
public ContextBoundObject
{
};

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

Об объектах, разделяющих одинаковое состояние, говорят как о живущих в том же самом контексте. Например, два объекта, которые не должны быть синхронизированы, могут совместно использовать один и тот же контекст. ContextBoundOb j ect и контексты обсуждаются более подробно в разделе, посвященном контекстам.

Используя это интуитивное понятие контекста, мы можем теперь объяснить назначение различных атрибутов синхронизации Synchronization (Синхронизация). NOT_SUPPORTED означает, что класс не может поддерживать синхронизацию своих методов экземпляров и нестатических полей и поэтому не должен создаваться в контексте синхронизации. REQUIRED (ТРЕБУЕМЫЙ) значит, что класс требует синхронизации доступа к своим методам и полям экземпляров. Однако, если поток уже синхронизирован, он может использовать существующий замок синхронизации и жить в существующем контексте синхронизации. REQUIRES_NEW означает, что требуется не только синхронизация, но также и доступ к методам и полям экземпляра должен происходить с уникальным замком синхронизации и в отдельном контексте. Атрибут SUPPORTED (ПОДДЕРЖИВАЕМЫЙ) применяется, если класс не требует синхронизации доступа к методам и полям экземпляра; для него не нужно создавать новый контекст.

Можно также передать булев флажок в конструктор, чтобы указать, требуется ли реентерабельность. Если флажок установлен (т.е. реентерабельность требуется), синхронизируются обратные вызовы из методов. В противном случае синхронизируются только вызовы методов.

Используя атрибут Synchronization (Синхронизация) в методе Brocker :: Reserve (Брокер :: Резерв), можно отказаться от Monitor :: Enter (Монитор :: Войти) и Monitor :: Exit (Монитор :: Выйти).

Так же, как и в шаге 2, в этом примере иллюстрируется попытка дважды зарезервировать последний свободный номер в гостинице. Вот как выглядит выдача при выполнении этого примера:

Added Boston Presidential Hotel with one room.
Thread 13 starting a new thread.
Thread 28 starting.
Thread 29 starting.
Reserving for Customer 1 at the Boston Presidential Hotel on
12/12/2001 12:00:00 AM for 3 days
Thread 28 entered Broker::Reserve
Thread 28 finds the room is available in Broker::Reserve
Thread 28 left Broker::Reserve
Reservation for Customer 1 has been booked
Reservationld = 1 ReservationRate = 10000
ReservationCost = 30000
Comment = OK
Reserving for Customer 2 at the Boston Presidential Hotel on
12/13/2001 12:00:00 AM for 1 days
Thread 29 entered Broker::Reserve
Thread 29 left Reserve.
Reservation for Customer 2 could not be booked
Room not available
Done !

Перевод такой:

Добавлена Бостонская Президентская гостиница с одним номером.
Поток 13 стартовал новый поток.
Поток 28 стартовал.
Поток 29 стартовал.
Резервирование для Клиента 1 в Бостонской Президентской гостинице
на
12/12/2001 12:00:00 AM в течение 3 дней
Поток 28 вошел в Брокер::Резерв
Поток 28 находит, что номер доступен в Брокер::Резерв
Поток 28 вышел из Брокер::Резерв
Резервирование для Клиента 1 было заказано
Reservationld = 1
ReservationRate = 10000
ReservationCost = 30000
Комментарий = OK
Резервирование для Клиента 2 в Бостонской Президентской гостинице на
12/13/2001 12:00:00 AM в течение 1 дня
Поток 29 вошел в Брокер::Резерв
Поток 29 вышел из Резерв.
Резервирование для Клиента 2 не могло быть заказано
Номер не доступен
Сделано!

Как и в предыдущем случае, второй поток не может выполнить метод Reserve (Резерв), пока поток, который начал выполнение первым, не закончится. Номер резервируется только раз.

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

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

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

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

Исключение, сгенерированное одним потоком, не приведет к отказу в работе другого потока. Пример Threadlsolation демонстрирует это.

_gc class tm
// класс сборщика мусора tm
{
public:
void m() {
Console::WriteLine(
"Thread {0} started", // "Поток {0} начал работу",
Thread::CurrentThread->GetHashCode().ToString());
Thread::Sleep(1000); // Поток:: Бездействует forfint i = 0; i < 10; i++)
Console::WriteLine(i); Console::WriteLine(
"Thread {0} done",
// "Поток {0} закончил",
Thread::CurrentThread->GetHashCode{).ToString());
}
};
_gc class te
// класс сборщика мусора te
{
public:
void tue ()
{
Console::WriteLine (
"Thread {0} started", // "Поток {0} начал работу",
Thread::CurrentThread->GetHashCode().ToString());
Exception *e = new Exception("Thread Exception");
// Исключение *е = новое Исключение ("Исключение Потока"); throw e;
}
};
_gc class Threadlsolation
// класс сборщика мусора Threadlsolation
{
public:
static void Main()
{
tm *tt = new tm;
// новый tm te *tex = new te;
// новый te
// создать делегат для потоков
ThreadStart *tsl = new ThreadStart(tt, tm::m);
ThreadStart *ts2 = new ThreadStart(tex, te::tue);
Thread *threadl = new Thread(tsl);
// новый Поток (tsl) Thread *thread2 = new Thread(ts2);
// новый Поток (ts2)
Console::WriteLine(
"Thread {0} starting new threads.",
// "Поток {0} запускает новые потоки.",
Thread::CurrentThread->GetHashCode().ToString());
threadl->Start(); // Пуск
thread2->Start(); // Пуск
Console::WriteLine(
"Thiead {0} done.", // "Поток {0} закончил.",
Thread::CurrentThread->GetHashCode().ToString());
}
};

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

Thread 2 starting new threads.
Thread 2 done.
Thread 5 started
Thread 6 started
Unhandled Exception: System.Exception:
Thread Exception at te.tue() in
c:\oi\netcpp\chap8\threadisolation\
threadisolation.h:line 32 0
1
2
3
4
5
6
7
8
9
Thread 5 done

Перевод такой:

Поток 2 запускает новые потоки. Поток 2 работу закончил. Поток 5 стартовал Поток 6 стартовал
Необработанное Исключение:
Система.Исключение: Исключение Потока в te.tue () в
c:\oi\netcpp\chap8\threadisolation\
threadisolation.h:line 32 О
1
2
3
4
5
6
7
8
9
Поток 5 работу закончил

Класс AppDomain (Прикладная область) (обсуждаемый в данной главе позже) позволяет устанавливать обработчик для перехвата исключения UnhandledException.

Некоторые списки, например TraceListeners, являются безопасными с точки зрения работы с потоками. Во время изменения этой коллекции изменяется копия и устанавливается ссылка на копию. Большинство коллекций, подобных ArrayList (Список массивов), небезопасны при работе с потоками. Но, сделав их автоматически безопасными для работы с потоками, мы уменьшили бы эффективность работы с коллекциями, даже если бы не стоял вопрос о безопасности.

ArrayList (Список массивов) содержит статический метод Synchronization (Синхронизация) для возврата к безопасной, с точки зрения работы с потоками, версии ArrayList (Список массивов). Свойство IsSynchronized позволяет проверить, безопасна ли используемая версия ArrayList (Список массивов) с точки зрения работы с потоками. Свойство SyncRoot возвращает объект, который может использоваться для синхронизации доступа к коллекции. Это позволяет синхронизировать потоки, которые могут использовать ArrayList (Список массивов), с помощью одного и того же объекта.

И Чтобы понять, как с помощью атрибутов среда времени выполнения реализует требования организации поточной обработки, мы должны ввести понятие контекста. Шаг 4 из примера Threading (Организация поточной обработки) состоит из того же кода, что и шаг 3, но в выдаче есть дополнительная информация:

Is the customer object a proxy? False
Is the bookings object a proxy? True
Added Boston Presidential Hotel with one room.
Thread 13 Contextld 0 launching threads.
MakeReservation: Thread 28 Contextld 0 starting.
MakeReservation: Thread 29 Contextld 0 starting.
Reserving for Customer 2 at the Boston Presidential Hotel on
12/13/2001 12:00:00
AM for 1 days
Thread 29 Contextld 1 entered Reserve.
Thread 29 finds the room is available in Broker::Reserve
Reserving for Customer 1 at the Boston Presidential Hotel on
12/12/2001 12:00:00
AM for 3 days
Thread 29 Contextld 1 left Reserve.
Thread 28 Contextld 1 entered Reserve.
Thread 28 Contextld 1 left Reserve.
Reservation for Customer 1 could not be booked
Room not available
Reservation for Customer 2 has been booked
Reservationld = 1
ReservationRate = 10000
ReservationCost = 10000
Comment = OK

Перевод такой:

Объект Клиент является агентом? Ложь
Заказывающий объект является агентом? Истина
Добавлена Бостонская Президентская гостиница с одним номером.
Поток 13 Contextld 0 запускает потоки.
MakeReservation: Поток 28 Contextld 0 стартовал.
MakeReservation: Поток 29 Contextld 0 стартовал.
Резервирование для Клиента 2 в Бостонской Президентской гостинице на
12/13/2001 12:00:00
AM в течение 1 дня
Поток 29 Contextld 1 вошел в Резерв.
Поток 29 находит, что номер доступен в Брокер::Резерв
Резервирование для Клиента 1 в Бостонской Президентской гостинице
на
12/12/2001 12:00:00
AM в течение 3 дней
Поток 29 Contextld 1 вышел из Резерва.
Поток 28 Contextld 1 вошел в Резерв.
Поток 28 Contextld 1 вышел из Резерва.
Резервирование для Клиента 1 не могло быть заказано
Номер не доступен
Резервирование для Клиента 2 было заказано
Reservationld = 1
ReservationRate = 10000
ReservationCost = 10000
Комментарий = OK

На этом последнем шаге (шаг 4) примера Threading (Организация поточной обработки) мы видим, что во время выполнения метода класса Broker (Брокер), такого как Reserve (Резерв), поток имеет идентификатор контекста Contextld, который отличается от идентификатора контекста во время выполнения какого-нибудь метода вне класса. Поток выполняется в различных контекстах. Это объясняет, почему в вышеприведенной распечатке поток 28, у которого идентификатор контекста Contextld был равен О перед вызовом метода Reserve (Резерв), изменяет свой идентификатор контекста Contextld на 1 во время выполнения данного метода. Такое поведение обусловлено применением атрибута Synchronization (Синхронизация) к классу Broker (Брокер).

Объекты класса Broker (Брокер) имеют отличные от других объектов требования во время выполнения. Поэтому доступ к объектам Broker (Брокер) должен быть синхронизирован, а доступ к другим объектам — нет. Среда, которая реализует требования какого-либо объекта во время выполнения, называется контекстом. В примере Threading (Организация поточной обработки) на шаге 3 и шаге 4 реализуются два контекста: контекст 1, в котором живет объект Broker (Брокер) и контекст 0, в котором сосуществуют все другие объекты. Каждый поток в программе будет выполняться либо в контексте 1 во время выполнения методов объекта класса Broker (Брокер), либо в контексте 0 в остальных случаях. Контекст не зависит от выполняющихся в нем потоков.

Контекст — совокупность одного или более объектов с идентичными требованиями параллелизма. Концепция контекста в .NET подобна концепции апартаментов (apartment) модели компонентных объектов Microsoft (COM) и концепции контекста в СОМ+. Вообще нельзя предсказать, что нужно делать во время выполнения в рамках заданного контекста. Это зависит от требований, предъявляемых во время выполнения. Контекст с требованиями, касающимися проведения транзакций, диктует иные действия, чем контекст, к которому такие требования не предъявляются. Или, если контекст поддерживает атрибут REQUIRED (ТРЕБУЕМЫЙ) в требованиях к синхронизации, то он отличается от контекста, поддерживающего в требованиях к синхронизации атрибут REQUIRES_NEW.

Экземпляр класса Context (Контекст), который представляет текущий контекст, можно получить с помощью статического свойства Thread: :CurrentContext. Context Id — это свойство класса Context (Контекст).

Как же во время выполнения удается удовлетворить различные требования различных контекстов? Когда какой-нибудь объект располагается в другом контексте (например, объект HotelBroker в экземпляре NewResevation), взамен указателя непосредственно на сам объект, возвращается указатель на некоторый заместитель. Реальный объект постоянно находится в своем первоначальном контексте. Заместитель — объект, который представляет первоначальный объект в другом контексте. Статический метод Remoting-Services: : IsTransparentProxy определяет, на что ссылается указатель: на реальный объект или на заместитель. Рассмотрим код в Threading.h на шаге 4 проекта TestThreading:

bool bTrans; // логическая булева переменная
bTrans bTrans = RemotingServices::IsTransparentProxy(
customers); // клиенты
Console::WriteLine (
"Is the customer object a proxy? {0}",
// "Объект Клиент представляет заместитель? {0}",
bTrans.ToString());
bTrans = RemotingServices::IsTransparentProxy(
hotelBroker) ; Console::WriteLine(
"Is the bookings object a proxy? {0}",
// "Заказывает заместитель? {О}",
bTrans.ToString());

Этот код генерирует следующую выдачу:

Is the customer object a proxy? False Is the bookings object a proxy? True

Перевод такой:

Объект Клиент представляет заместитель? Ложь
Заказывает заместитель? Истина

При запуске программа выполняется в определенном по умолчанию контексте. Все объекты, такие как Customers (Клиенты), не предъявляющие специальных требований, создаются внутри этого контекста (контекст 0). Какой-нибудь объект, такой как HotelBroker с другими требованиями к синхронизации, создается в другом контексте (контекст 1), а заместитель возвращается в контекст 0.

Теперь, запрашивая метод MakeReservation объекта HotelBroker, мы реально получаем доступ к методу заместителя. Метод заместителя может потом применить замок синхронизации и затем делегировать функции методу реального объекта HotelBroker. Метод настоящего объекта возвращает управление заместителю. А заместитель может тогда удалить замок синхронизации и возвратить управление вызвавшей его программе. Такая технология, когда во время выполнения используются заместители для перехвата вызовов методов реальных объектов, называется перехватыванием.

Можно также просмотреть подобный пример под названием MarshalByReference в папке с примерами. Он показывает различные способы создания объекта и эффект от создания заместителя.

Класс Broker (Брокер) должен быть производным от класса ContextBoundObject, чтобы во время выполнения можно было определить, требуется ли установить новый контекст. Если бы класс Broker (Брокер) не был производным от класса ContextBoundObject, то мы опять получили бы конфликт при организации поточной обработки: оба клиента смогли бы зарезервировать один и тот же единственный последний номер гостиницы. Это произошло бы даже несмотря на то, что класс был бы помечен атрибутом Synchronization (Синхронизация). А объект, не являющийся производным от ContextBoundOb j ect, сможет работать в любом контексте (подвижной объект).

Поскольку другие контексты работают с заместителями или ссылками на реальный объект, вызовы из одного контекста в другой необходимо преобразовывать (приспосабливать) во время выполнения. Поэтому ContextBoundObject является производным от MarshalByRefObject. MarshalByRefOb]ect — это базовый класс для объектов, которые нужно адресовать по ссылке.

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

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

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

К сожалению, использование отдельных процессов с целью достижения изоляции частей приложения — дорогое удовольствие для отдельного приложения. Для переключения от одного процесса к другому должен быть сохранен, а впоследствии восстановлен контекст (информация о состоянии процесса). Такое действие включает в себя переключение между потоками и процессами. Для переключения между потоками требуется сохранить регистры центрального процессора, такие как стек вызовов и указатель команд.

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

Прикладная область .NET (Application Domain, иногда называемая AppDomain) — это более простой способ изоляции приложений, безопасный и устойчивый к ошибкам. В рамках одного процесса может находиться несколько прикладных областей. Поскольку в .NET код проверяется на типовую безопасность и защищенность, общеязыковая среда времени выполнения CLR гарантирует, что прикладная область может выполняться независимо от других прикладных областей в рамках одного процесса. Для изоляции приложения переключать процессы не требуется.

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

Каждая прикладная область создается вместе с одним потоком в пределах одного контекста. Дополнительные потоки и контексты, в которых они выполняются, создаются по мере необходимости.

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

Чтобы изолировать приложения, код из одной прикладной области не должен непосредственно вызывать кода (или даже ссылаться на ресурсы) из других прикладных областей.

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

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

Для приложений, подобных ASP.NET или Internet Explorer, чрезвычайно важно (критично!) предотвратить взаимное вмешательство приложений, выполняющихся в рамках таких приложений. Код приложений никогда не загружается в прикладную область, создаваемую по умолчанию. Поэтому крах какого-либо из них не приведет к краху главного приложения.

Класс AppDomain (Прикладная область) реализует абстракцию прикладной области. Пример AppDomain (Прикладная область) иллюстрирует использование прикладных областей. Этот класс имеет статические методы для создания и разгрузки прикладных областей:

AppDomain *domain = AppDomain::CreateDomain(
"CreatedDomain2", 0, 0);
AppDomain::Unload(domain); // разгрузить область

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

static AppDomain CreateDomain( // статический
String *friendlyName, // Строка
Evidence *securitylnfo,
AppDomainSetup *info);

Параметр Evidence (Данные) — коллекция ограничений защиты для прикладной области. Этот параметр мы обсудим более подробно в главе 13 "Защита"; сейчас же мы отметим, что создатель прикладной области может модифицировать коллекцию, чтобы управлять разрешениями выполняющейся прикладной области. Параметр AppDomainSetup определяет параметры прикладной области. В параметрах указывается путь к конфигурационному файлу прикладной области и информация о том, куда загружены приватные сборки. Поэтому прикладные области могут быть сконфигурированы независимо одна от другой. Изоляция кода, изоляция параметров и управление защитой в совокупности гарантируют, что прикладные области остаются независимы друг от друга.

Для поддержки изоляции приложений класс AppDomain (Прикладная область) позволяет устанавливать обработчики для следующих событий:

  • выгрузка прикладной области;
  • завершение процесса;
  • возникновение необрабатываемого исключения;
  • сбой при попытке разрешить сборки, типы или ресурсы.

При выполнении примера AppDomain (Прикладная область) получается выдача, показанная на рис. 8.1.

Рис. 8.1. Выдача в примере AppDomain (Прикладная область)

Сначала выводится имя, поток и контекст прикладной области, заданной по умолчанию

AppDomain *currentDomain = AppDomain:: CurrentDomam;
Console::WriteLine(
"At startup, Default AppDomain is {0}
Threadld: {1}
"При запуске по умолчанию AppDomain - {0}
Threadld: {1}
Contextld {2}\n",
currentDomain->FrlendlyName,
Thread::CurrentThread->GetHashCode() .ToString(),
Thread::CurrentContext->ContextID.ToString());

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

int val = domain->ExecuteAssembly(
"TestApp\\bin\\Debug\\TestApp.exe", 0, args);
// параметры

Потом мы создадим экземпляр Customers (Клиенты) из сборки Customer (Клиент) в прикладной области, заданной по умолчанию Метод Createlnstance класса AppDomain (Прикладная область) возвращает экземпляр ObjectHandle Этот ObjectHandle можно передавать между прикладными областями без загрузки метаданных, ассоциированных с упакованным типом Если нужно использовать созданный экземпляр объекта, его следует распаковать, вызвав метод Unwrap (Развернуть) для объекта Ob] ectHandle

Objecthandle *on = currentDomain->Create!nstance(
"Customer", "01.NetCpp.Acme.Customers"), // Клиент
Customers *custs = // Клиенты
dynamic_cast(oh->Unwrap ());

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

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

AppDomain *domain = AppDomain::CreateDomain(
"CreatedDomainl", 0, 0);
oh = domain->CreateInstance(
"Customer", "01.NetCpp Acme.Customers"); // Клиент
Customers *custs2 = dynamic_cast // Клиенты
(oh->Unwrap());

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

Когда мы составляем список клиентов в этом новом объекте, мы получаем другой список клиентов И не удивительно, ведь это другой объект Customers (Клиенты) Тем не менее, метод составления списка клиентов выполняется в заданной по >молчанию прикладной области1

Используя RemotingServices: : IsTransparentProxy, можно увидеть, что ObjectHandle — это заместитель объекта Customers (Клиенты) в новой созданной прикладной области (AppDomain) Однако, если попытаться распаковать объект, чтобы получить дескриптор экземтяра, то в результате мы получим не указатель на заместитель, а фактическую объектную ссылку По умолчанию, объекты передаются по значению (копируются) из одной прикладной области в другую

Если объект Customers (Клиенты) — нельзя сериализовать (преобразовать в последовательную форму), то при попытке его скопировать будет запущено исключение Это исключение появляется при вызове метода Unwrap (Развернуть), а не Createlnstance Последний метод возвращает ссылку Копирование происходит только в том случае, если объект ObjectHandle распакован Если же объект не может быть преобразован в поспедоватечьную форму, он не может быть скопирован из одной прикладной области в другую

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

AppDomainTest
AppDomain *domain = AppDomain::CreateDomain( "CreatedDomain2", 0, 0);
String * args[] = new String *[!];
// Строка * параметры [] = новая Строка * [1];
args[0] = "MakeReservation"; // параметры [О]
int val = domain->ExecuteAssembly(
"TestApp\\bin\\Debug\\TestApp.exe", 0, args); // параметры
AppDomain::Unload(domain);

Подпрограмма Main (Главная) загружает сборку Hotel (Гостиница) в созданную прикладную область. В этом примере приложение TestApp. exe реализовано на С#. После загрузки оно запрашивает метаданные сборки для получения информации о типе HotelBroker. Затем эта информация используется для создания объекта HotelBroker. Класс HotelBroker помечен атрибутом синхронизации. В результате конструктор HotelBroker и метод MakeReservation работают в контексте, отличном от заданного по умолчанию.

Assembly a = AppDomain.CurrentDomain.Load("Hotel") ;
// Сборка - Загрузить ("Гостиница"); Type typeHotelBroker =
а.GetType("01.NetCpp.Acme.HotelBroker"); HotelBroker hotelBroker =
(HotelBroker)Act vator.CreateInstance(typeHotelBroker); DateTime date = Date
Time.Parse("12/2/2001"); // дата = DateTime.
Синтаксический анализ ("12/2/2001");
ReservationResult rr = hotelBroker.MakeReservation(1,
"Boston", "Sheraton", date, 3);
// "Бостон", "Шератон", дата, 3);
Console.WriteLine("\tReservation Id: {0}", // Идентификатор
rr.Reservationld);

По умолчанию объекты копируются из одной прикладной области в другую (передаются по значению). В разделе "Удаленный доступ" показано, как происходит передача по ссылке между прикладными областями. Это гарантирует изоляцию кода одной прикладной области от кода другой.

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

Поскольку большинство объектов не являются производными от ContextBoundOb j ect, они могут постоянно храниться в одном контексте или перемещаться при необходимости из одного контекста в другой. При этом потоки могут пересекать границы прикладных областей или контекстов в пределах одного и того же процесса Win32.

.NET поддерживает шаблон проектирования для асинхронного программирования. Этот шаблон используется во многих местах .NET (включая операции ввода-вывода, как было отмечено ранее, и как будет более подробно показано в главе 11 "Web-службы"). Асинхронное программирование может вызывать метод без блокирования вызывающей программы метода. С точки зрения клиента использовать асинхронную модель проще, чем потоки. Однако она предоставляет гораздо меньше возможностей управления синхронизацией по сравнению с использованием синхронизирующих объектов. Поэтому разработчики классов, вероятно, сочтут, что использовать потоки намного легче.

Такой шаблон проектирования состоит из двух частей: набора методов и интерфейса lAsyncResult. Методы шаблона имеют следующий вид:

lAsyncResult *BeginXXX(
[InputParams], AsyncCallback *cb, Object *AsyncObject)
[ReturnValue] EndXXX([OutputParams], lAsyncResult *ar);

В шаблоне проектирования XXX обозначает реальный метод, который вызывается асинхронно (например, BeginRead/EndRead для класса System: : IO::FileStream). BeginXXX должен принимать те же входные параметры, что и его синхронный аналог (in, in/out и ref), а также параметры AsyncCallback и AsyncObject. В сигнатуре EndXXX должны присутствовать все выходные параметры (ref, out, in/out) синхронной версии. Этот метод должен возвратить любой объект или значение, которое возвращает синхронная версия данного метода. Он должен также иметь параметр lAsyncResult. Может быть предусмотрен и метод Cancelxxx, если в нем имеется необходимость.

AsyncCallback — делегат, который представляет функцию обратного вызова. public _delegate void AsyncCallback(lAsyncResult *ar);

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

Каркас использует данный шаблон таким образом, чтобы синхронный метод Read (Чтение) класса FileStream можно было использовать асинхронно. Ниже приведено описание синхронного метода FileStream: : Read:

int Read( // Чтение
_in unsigned char* array _gc[],
int offset, int count);
// int смещение, int счетчик);

А вот асинхронная версия, используемая в шаблоне проектирования:

lAsyncResult *BeginRead(
_in unsigned char* array _gc[],
int offset, int numBytes,
// смещение,
AsyncCallback *userCallback,
Object *stateObject); // Объект
int EndRead(lAsyncResult *asyncResult);

Любое исключение, которое запускается в BeginXXX, должно быть запущено до начала выполнения асинхронных операций. Каждое исключение, связанное с асинхронными операциями, должно быть запущено из метода EndXXX.

Метод BeginXXX (такой как BeginRead) возвращает lAsyncResult. Следующий интерфейс содержит четыре элемента:

public _gc _interface lAsyncResult
// сборщик мусора - интерфейс lAsyncResult
{
public:
bool get_IsCompleted(); // логический (булев)
bool get_CompletedSynchronously();// логический (булев)
WaitHandle* get_AsyncWaitHandle();
Object* get_AsyncState(); }

Полю get_IsCompleted будет присвоено значение true (истина) после обработки вызова сервером. Клиент может уничтожить все ресурсы после того, как get_IsCompleted получит значение true (истина). Полю get_CompletedSynchronously будет присвоено значение true (истина), если BeginXXX заканчивается синхронно. Чаще всего это поле игнорируется и по умолчанию его значение устанавливается равным false (ложь). Обычно клиент даже не знает, выполняется ли метод BeginXXX синхронно или асинхронно. Если асинхронная операция не закончена, метод EndXXX блокируется до завершения выполнения операции.

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

Объект get_AsyncState — последний параметр в вызове метода BeginXXX. Он позволяет дифференцировать процессы чтения в асинхронном режиме при обратном вызове.

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

Два примера Asynch используют объект Customers (Клиенты) из изученной нами сборки Customer (Клиент). Первый пример, AsynchWithoutCallback, асинхронно регистрирует новых клиентов и выполняет обработку во время ожидания завершения каждой регистрации. Второй пример, AsynchWithCallback (Asynch с обратным вызовом), использует функцию обратного вызова для асинхронной обработки данных. В дополнение к тому, что программа может обрабатывать данные во время ожидания завершения регистрации, обратный вызов позволяет системе произвести некоторое асинхронное действие для каждой отдельной регистрации.

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

Режим ожидания) в Customer: : RegisterCustomer и в программу-пример. Теперь рассмотрим код в примерах.

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

public _delegate int RegisterCustomerCbk(
String *FirstName, // Строка
String *LastName, // Строка
String *EmailAddress); // Строка

Таким образом реальный метод превращается в функцию обратного вызова:

RegisterCustomerCbk *rcc = new
RegisterCustomerCbk{
customers, // клиенты
Customers::RegisterCustomer); // Клиенты

Вызов (invoke) начала (Begin) и конца (End)

При объявлении делегата компилятор генерирует класс с тремя методами: Beginlnvoke, Endlnvoke и Invoke (Вызвать). Beginlnvoke и Endlnvoke — методы с типовой безопасностью. Они соответствуют методам BeginXXX и EndXXX и позволяют вызывать делегат асинхронно. При вызове делегата компилятор использует метод Invoke (Вызвать)27. Чтобы асинхронно вызвать RegisterCustomer, достаточно использовать только методы Beginlnvoke и Endlnvoke.

RegisterCustomerCbk *rcc = new RegisterCustomerCbk(
customers, // клиенты
Customers::RegisterCustomer); // Клиенты for(int i = 1; i < 5; i++) {
firstName = String::Concat( // Строка
"FirstName", i.ToString ());
lastName = String::Concat( // Строка
"SecondName", (i * 2).ToString{));
emailAddress = String::Concat ( // Строка
i.ToString(), ".biz"); lAsyncResult *ar =
rcc->Begin!nvoke( firstName, lastName, emailAddress, 0, 0) ;
while(!ar->IsCompleted)
{
Console::WriteLine(
"Could do some work here while waiting for customer
registration to complete.");
// "Могу сделать некоторую работу здесь
//во время ожидания
// завершения регистрации клиента.");
ar->AsyncWaitHandle->WaitOne(1, false);
}
customerld = rcc->End!nvoke(ar) ;
Console::WriteLine(
Added Customerld: {0}",
// " Добавлен Customerld: {0}"
customerId.ToString());
}

Программа периодически ждет AsyncWaitHandle, чтобы увидеть, закончилась или нет регистрация. Если нет, то тем временем программа могла бы выполнить некоторую работу. Если Endlnvoke вызывается до завершения RegisterCustomer, то выполнение блокируется до завершения RegisterCustomer.

Асинхронный обратный вызов

Вместо ожидания на дескрипторе, можно передать функцию обратного вызова в Beginlnvoke (или BeginXXX) метод. Именно так и делается в примере AsynchWithCallback (Asynch с обратным вызовом).

RegisterCustomerCbk *rcc = new RegisterCustomerCbk( customers, // клиенты
Customers::RegisterCustomer);
// Клиенты AsyncCallback *cb =
new AsyncCallback(this, CustomerCallback);
Object *objectState; // Объект
JAsyncResult *ar; forUnt i = 5; i < 10; i++) {
firstName = String::Concat( // Строка
"FirstName", i.ToString());
lastName = String::Concat( // Строка
"SecondName", (i * 2).ToString());
emailAddress =
String::Concat(i.ToString(), ".biz"); // Строка
objectState = _box(i);
ar = rcc->Begin!nvoke( firstName,
lastName,
emailAddress,
cb,
objectState);
}
Console::WriteLine(
"Finished registrations...
could do some work here.");
// "Закончена регистрация... могу сделать
// некоторую работу здесь."
Thread::Sleep(25); // Поток:: Бездействие
Console::WriteLine(
"Finished work..waiting to let registrations complete.");
// "Закончена работа., жду конца регистрации.");
Thread::Sleep(1000); // Поток:: Бездействие

Потом мы получаем результат в функции обратного вызова:

void CustomerCallback(lAsyncResult *ar)
{
int customerld;
AsyncResult *asyncResult =
dynamic_cast(ar);
RegisterCustomerCbk *rcc =
dynamic_cast (asyncResult->AsyncDelegate);
customerld = rcc->EndInvoke(ar);
Console::WriteLine(
AsyncState: {0} Customerld {1} added.",
ar->AsyncState, customerld.ToString() ) ;
Console::WriteLine(
" Could do processing here.");
// " Могу сделать обработку здесь."
return;
}

В этом варианте можно выполнить некоторые действия до завершения регистрации каждого клиента.

Асинхронный обратный вызов выполняется в потоке, отличном от того, в котором был сделан вызов Beginlnvoke. Если требования к организации поточной обработки просты, то для передачи параметров функциям потока можно использовать асинхронные делегаты. Ссылка на пространство имен Threading (Организация поточной обработки) не нужна. Ссылка на это пространство имен в примере AsynchThreading нужна только для метода Thread: : Sleep (Поток::Режим ожидания), используемого в иллюстративных целях.

PrintNumbers суммирует числа от начального целого числа, переданного в качестве параметра, до числа, на 10 большего чем начальное целое число. Результат (сумма) возвращается вызывающей программе. PrintNumbers может использоваться для делегата, определенного как Print (Печать).

//AsynchThreading.h
using namespace System;
// использование пространства имен Система;
using namespace System::Threading;
// использование пространства имен Система::Потоки;
public _delegate int Print(int i);
// делегат int Печать (int i);
public _gc class Numbers
// класс сборщика мусора - Числа
{
public:
int PrintNumbers(int start) // начало
{
int threadld = Thread::CurrentThread->GetHashCoae();
// Поток
Console::WriteLine (
"PrintNumbers Id: {0}",
// " Идентификатор PrintNumbers: {0}"
threadld.ToString() ) ;
int sum =0; // сумма = О for (int i = start; i < start + 10; i++)
{
Console::WriteLine(i.ToString());
Thread::Sleep(500);
// Поток:: Бездействие sum += i;
// суммируем
}
return sum;
// сумма
}

Функция Main (Главная) определяет два обратных вызова и вызывает их явно с различными начальными целыми числами. Она ждет, пока оба из дескрипторов синхронизации не получат сигнал. Endlnvoke вызывается для обоих и результат выводится на консоль.

Numbers *n = new Numbers;
// новые Числа
Print *pfnl = new Print(n, Numbers::PrintNumbers);
// новая Печать
Print *pfn2 = new Print (n, Numbers::PrintNumbers};
// новая Печать lAsyncResult *arl =
pfnl->Begin!nvoke(0, 0, 0);
lAsyncResult *ar2 =
pfn2->Begin!nvoke(100, 0, 0);
WaitHandle *wh [] = new WaitHandle*[2];
wh[0] = arl->AsyncWaitHandle;
wh[l] = ar2->AsyncWaitHandle;
// удостоверимся, что все сделано перед окончанием
WaitHandle::WaitAll(wh);
int suml = pfnl->End!nvoke(arl);
int sum2 = pfn2->End!nvoke(ar2);
Console::WriteLine(
"Suml = {0} Sum2 = {!}",
suml.ToString(), sum2.ToString());

Вот выдача программы.

MainThread Id: 2 // Идентификатор
PrintNumbers Id: 14 // Идентификатор 0
1
2
3
4
5
6
7
8
9
PnntNumbers Id: 14 // Идентификатор
100
101
102
103
104
105
106
107
108
109
Suml =45 Sum2 =1045

Технология удаленного доступа использует все ключевые концепции прикладной модели .NET (.NET Application Model). Хотя подробное обсуждение удаленного доступа не является целью этой книги, в кратком введении мы представим хороший пример использования метаданных и передачи по ссылке (marshal by reference, MBR) Удаленный доступ представляет собой механизм, который поддерживает исполняемые серверы

В отличие от удаленного доступа в технологии, основанной на модели компонентных объектов Microsoft (COM), в .NET для программирования инфраструктуры требуются минимальные усилия Необходимая инфраструктура программы позволяет программисту выбирать степень гибкости, т.е способность настраивать удаленный доступ для своих собственных приложений

Каркас NET Framework предоставляет два способа устанавливать соединения между двумя прикладными программами на различных компьютерах Рассматриваемые в главе 11 "Web-службы" сетевые службы позволяют компьютерам, на которых не установлена общеязыковая среда времени выполнения CLR, связываться с компьютерами, на которых общеязыковая среда времени выполнения CLR установлена Технология удаленного доступа, обсуждаемая здесь, позволяет создать распределенное приложение при условии, что на компьютерах установлена общеязыковая среда времени выполнения CLR.

Ниже перечислены ключевые части удаленного доступа

  • перехват, с помощью которого генерируются сообщения для связи по каналам;
  • форматеры (Formatters), служащие для помещения сообщения в байтовый поток, который посылается по каналу Это те же самые форматеры, которые обсуждались ранее в разделе, посвященном сериализации (преобразованию в последовательную форму),
  • каналы связи для транспортировки (передачи) сообщений
  • Перехват

Заместители и заглушки (называемые в NET диспетчерами, организующими программами и планировщиками) преобразовывают обращение к функции на клиентской или серверной стороне в сообщения, которые посылаются по сети Такой процесс называется перехватом, потому что заместители и диспетчеры перехватывают вызов метода для посылки его удаленному адресату. В отличие от модели компонентных объектов Microsoft (COM), общеязыковая среда времени выполнения CLR может генерировать заместители и заглушки с помощью информации, хранимой в метаданных.

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

Например, предположим, что метод UnregisterCustomer из сборки Customer (Клиент) выполняется в одной прикладной области и вызывается из другой. Нет никакой разницы, если прикладные области находятся в том же самом процессе или на той же самой машине.

Заместитель принимает целочисленный параметр id (идентификатор) из стекового фрейма клиента, который делает вызов и помещает его в сообщение, кодирующее запрос и его параметр. На серверной стороне диспетчер принимает это сообщение и помещает обращение к функции в стек сервера для вызова UnregisterCustomer (int id), a также преобразует этот вызов объекта. Коды клиента и сервера могут даже не знать, что они удалены друг от друга.

Каналы и форматеры

Форматер преобразовывает сообщение в поток байтов. Каркас .NET Framework поставляется с двумя форматерами, двоичным и SOAP (использующим XML-документы и рассматриваемым в главе 11 "Web-службы"). Поток байтов затем посылается по каналу связи.

Каркас .NET Framework поставляется также с двумя каналами, хотя можно создать и свой собственный. Канал HTTP, использующий протокол передачи гипертекстовых файлов HTTP хорош для соединения по Internet или через брандмауэры. Канал TCP использует протокол управления передачей TCP (сокеты) и предназначен для высокоскоростной связи. Таким образом, имеются четыре возможных перестановки форматеров и каналов транспортировки: двоичный по протоколу управления передачей TCP, двоичный по протоколу передачи гипертекстовых файлов HTTP, SOAP по протоколу управления передачей TCP и SOAP по протоколу передачи гипертекстовых файлов HTTP.

Клиентская часть организовывает заместитель, активизируя (вызывая) удаленный объект. Удаленные объекты должны быть производными от MarshalByRefObject, потому что вы работаете с заместителем объектной ссылки, а не непосредственно с самой объектной ссылкой. Это та же концепция, что и ранее при обсуждении понятия контекста, где для обращения к объектам, находящимся в другом контексте, также используется передача по ссылке.

Локальные объекты, передаваемые в качестве параметров методов из одной прикладной области в другую, можно передавать по значению (копировать) или по ссылке.

Чтобы передать объект по значению, он должен быть сериализован (преобразован в последовательную форму). Объект преобразуется в последовательную форму, передается на транспортном уровне, и воссоздается на другой стороне. Это мы уже видели в примере AppDomain (Прикладная область).

Чтобы передать класс по ссылке, он должен быть производным от MarshalByRef Object. Пример Remoting иллюстрирует передачу объекта по ссылке.

Удаленные объекты могут быть активированы как сервером, так и клиентом. Активизированные сервером объекты не создаются до вызова первого метода этого объекта. Они подразделяются на две разновидности, SingleCall и Singleton (Одноэлементное [одноточечное] множество). SingleCall — это объект без состояния, т.е. объект, не меняющий свое состояние в процессе выполнения. Вызов каждого метода приводит к созданию нового объекта. Другая разновидность объектов. Singleton (Одноэлементное [одноточечное] множество), работает иначе. Один и тот же экземпляр Singleton (Одноэлементное [одноточечное] множество) объекта может использоваться для обслуживания нескольких клиентских запросов. Объекты Singleton (Одноэлементное [одноточечное] множество) могут иметь состояние. Объекты Singleton (Одноэлементное [одноточечное] множество) существуют постоянно. Использование объектов SingleCall предпочтительнее, чем объектов Singleton (Одноэлементное [одноточечное] множество), если нужна лучшая масштабируемость, потому что они не сохраняют состояние и при их использовании легче сбалансировать загрузку.

Вызванные клиентом объекты активизируются тогда, когда клиент их запрашивает. Хотя они могут использоваться для нескольких запросов и хранить свое состояние, но не могут хранить информацию о различных вызовах со стороны клиента. Это похоже на вызов CoCreatelnstanceEx в распределенной модели компонентных объектов DCOM.

Объекты активизируются на стороне клиента одним из трех способов, используя класс Activator (Активатор, Модуль активизации).

  • Activator: :GetObject используется для получения ссылки на активизированный сервером объект.
  • Activator: :Createlnstance используется для создания активизированного пользователем объекта. Параметры для конструктора можно передать, используя один из перегруженных методов Createlnstance, который принимает массив объектов для передачи их конструктору.
  • Синтаксис оператора new (создать) языка C++ может использоваться для создания активизированного сервером или клиентом объекта. Для описания способа применения new (создать) используется конфигурационный файл.

Для нашего примера удаленного доступа мы изолируем наш объект Customers (Клиенты) от сборки Customer (Клиент). В папке примера Remoting находятся два решения. Одно представляет собой клиентскую часть программы, другое — серверную. Сначала нужно скомпоновать серверную часть приложения. При этом также создастся Customer.dll. Необходимо скопировать эту динамически подключаемую библиотеку (DLL) в папку Debug (Отладка) для решений Server (Сервер) и Client (Клиент). Нужно обратить внимание, что сначала запускается программа-сервер, которая после запуска переходит в режим ожидания клиентского запроса. После этого можно выполнить клиентскую часть приложения, которая активизирует объекты, существующие внутри сервера. Подробности программного кода клиента и сервера мы обсудим в нескольких последующих разделах.

Отметим, что нужно было внести два простых изменения в наш объект. Класс Customers (Клиенты) в проекте сервера — удаленный (с возможностью удаленного доступа), — для этого мы сделали его производным от MarshalByRefObject.

public _gc class Customers :
// класс сборщика мусора Клиенты:
public MarshalByRefObject, public ICustomer

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

[Serializable]
// [Преобразуемый в последовательную форму]
public _value struct CustomerListltem
{
public:
int Customerld;
String *FirstName; // Строка
String *LastName; // Строка
String *EmailAddress; // Строка
};

В примере Remoting клиент обращается к активизированному сервером объекту. Сервер— это класс TcpServerChannel, использующий двоичный формат с протоколом управления передачей TCP. Канал будет использовать порт 8085. Сервер регистрирует удаленный тип, имя конечной точки для ссылки на этот объект и тип активизации. Потом сервер переходит в состояние ожидания клиентских запросов.

TcpServerChannel *chan = new TcpServerChannel(8085);
ChannelServices::RegisterChannel(chan); // канал
RemotingConfiguration::RegisterWellKnownServiceType(
_typeof(Customers), // Клиенты
"AcmeCustomer",
WellKnownObjectMode::Singleton); // Единичный предмет

Сервер должен быть запущен прежде, чем клиентская программа обратится к объекту. Клиент устанавливает объект TcpClientChannel и затем присоединяется к нему. . Он определяет требуемый тип объекта и конечную точку, в которой сервер будет ожидать запросы объекта. Если нужно выполнить клиент и сервер на отдельных машинах, необходимо указать имя машины сервера для локального узла в конечной точке. В отличие от прозрачности определения места расположения в модели компонентных объектов Microsoft (СОМ), клиент должен сам определить конкретную конечную точку. Здесь нет переадресации с помощью скрытой записи в системном реестре.

TcpClientChannel *chan = new TcpClientChannel;
ChannelServices::RegisterChannel(chan); // канал
Customers *obj = dynamic_cast( // Клиенты
Activator::GetObject( // Активатор
_typeof(Customers), // Клиенты
"tcp://localhost:8085/AcmeCustomer"));
if (obj == 0) // если (obj == 0)
Console::WriteLine("Could not locate server");
// ("He могу определить местонахождение сервера");
else
{

Потом клиент использует заместитель, чтобы вызвать объект так, как будто это локальный экземпляр.

bool bRet = // логический (булев)
RemotingServices::IsTransparentProxy(obj);
ArrayList *ar;
ar = obj->GetCustomer(-1);
ShowCustomerArray(ar);

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

Вывод зависит от того, какой объект активизирует сервер. Если на сервере активизируется Singleton (Одноэлементное [одноточечное] множество) — объект, который может иметь состояние, то получим поведение, какого можно ожидать в случае недистан-циированного объекта. Когда новый клиент добавлен, его можно обнаружить в списке, если сделать запрос обо всех существующих клиентах. Как и следовало ожидать, начальный активизирующий вызов приводит к тому, что конструктор Customers (Клиенты) вызывается один раз при каждом инициировании работы сервера, независимо от количества запусков клиентской программы.

Object reference a proxy?: True
Client: AppDomain Client.exe Thread 19 Context 0
1 Rocket Squirrel rocky@frosbitefalls.com
2 Bullwinkle Moose moose@wossamotta.edu
1 Rocket Squirrel rocky@frosbitefalls.com
2 Bullwinkle Moose moose@wossamotta.edu
3 Boris Badenough boris@no-goodnicks.com

Результаты весьма отличаются от приведенных выше, если тип активизации — SingleCall, при котором для каждого вызова метода создается новый экземпляр объекта. При этом создаются четыре различных объекта. Первый объект создан при начальном активизирующем запросе. Второй создается при начальном вызове метода GetCustomer. Третий объект создается вследствие вызова RegisterCustomer. И четвертый — в результате повторного вызова метода GetCustomer. Последний созданный объект никогда не увидит нового клиента, потому что состояние не сохраняется. Обратите внимание, что статический член nextCustld класса Customer (Клиент) обрабатывается как статический относительно новых экземпляров объектов класса Customer (Клиент), т.е. именно так, как того и следовало ожидать. Тот же программный код клиента, а результаты различные! Поскольку объект уже активизирован, при повторном выполнении клиентской программы для той же самой инициализации работы сервера, конструктор Customer (Клиент) будет вызван только трижды.

Object reference a proxy?: True
Client: AppDomain Client.exe Thread 19 Context 0
3 Rocket Squirrel rocky@frosbitefalls.com
4 Bullwinkle Moose moose@wossamotta.edu
8 Rocket Squirrel rocky@frosbitefalls.com
9 Bullwinkle Moose moose@wossamotta.edu

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

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

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

Один из возможных способов сделать это состоит в том, чтобы создать версию объекта, которая содержит методы без реализации. Этот интерфейсный класс можно потом встроить в сборку, которая предоставляется клиенту. Можно запускать исключение System: :NotSupportedException в методах, чтобы удостовериться в том, что они никогда не будут по ошибке использованы реальным объектом.

Для Web-служб можно использовать утилиту SOAPSUDS, с помощью которой извлекаются метаданные из службы, а затем сгенерировать сборку с требуемыми метаданными. Далее можно скомпилировать заместитель динамически подключаемой библиотеки (DLL), на который должна ссылаться программа-клиент. Концептуально это эквивалентно первому подходу. Сервер, конечно, ссылается на реальную сборку.

В отличие от модели компонентных объектов Microsoft (COM), здесь нет счетчика ссылок, создания и регистрирования отдельных заместителей и заглушек, согласования интерфейсов, беспокойства относительно глобальных идентификаторов и использования системного реестра. Благодаря метаданным для удаленного доступа к объекту необходимо только, чтобы этот объект был производным от MarshalByRef Object.

Конфигурационные файлы используются, чтобы определить, где будет активизирован объект. Клиент затем использует оператор new (создать) для создания объекта. Большое преимущество использования такого подхода заключается в том, что при изменении места расположения (например, унифицированного указателя информационного ресурса (URL) или канала TCP), либо при замене используемого форматера, не нужно будет перепрограммировать клиент.

Многие классы могут быть сконфигурированы клиентом. Файлы конфигурации клиент загружает с помощью метода RemotingConf iguration: : Configure.

В главе 5 "Управляемый C++ в .NET Framework" мы ввели концепцию атрибутов, которые уже появлялись в нескольких примерах. В этой главе мы использовали атрибуты Serializable (Преобразуемый в последовательную форму) и Synchronization (Синхронизация), которые предоставляются классами .NET Framework. Каркас .NET Framework делает механизм использования атрибутов полностью расширяемым, позволяя определять собственные произвольные атрибуты, которые могут быть добавлены к метаданным класса. Эти пользовательские метаданные доступны благодаря механизму отражения и могут быть использованы во время выполнения. Чтобы упростить использование самостоятельно определенных атрибутов, можно объявить базовый класс, который будет вызывать отражающий интерфейс прикладного программирования (API) для получения информации из метаданных.

В примере CustomAttribute иллюстрируется использование самостоятельно созданного атрибута InitialDirectory. InitialDirectory указывает начальный текущий каталог, из которого запускается программа. По умолчанию текущим каталогом является тот, в котором содержится решение, и в нашем случае это каталог С:\01\NetCpp\Chap08\CustomAttribute.

Перед тем как обсудить реализацию собственного атрибута, рассмотрим, как используется атрибут InitialDirectory. Чтобы указать начальный каталог для класса, мы сделаем класс производным от базового класса DirectoryContext. Тогда мы сможем применить к такому классу атрибут InitialDirectory, который принимает параметр типа String*, определяющий путь к начальному каталогу. Свойство DirectoryPath извлекает путь из метаданных. Если к нашему классу не применен атрибут, этот путь примет значение по умолчанию. Ниже приведен код нашей тестовой программы.

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

//AttributeDemo.h
using namespace System;
// использование пространства имен Система;
using namespace System::10;
// использование пространства имен Система::10;
_gc class Normal : public DirectoryContext
Т"
};
[InitialDirectory("С:\\OI\\NetCpp\\Chap08")
] _gc class Special : public DirectoryContext // класс сборщика мусора
Специальный:DirectoryContext
{
};
public _gc class AttributeDemo
// класс сборщика мусора AttributeDemo
{
public:
static void Main() {
Normal *objNormal = new Normal;
Console::WriteLine(
"path = {0}",
objNormal->DirectoryPath);
// путь = ShowDirectoryContents(objNormal->DirectoryPath) ;
Special *objSpecial = new Special;
// новый Специальный Console::WriteLine(
"path = {0}",
objSpecial->DirectoryPath);
// путь = ShowDirectoryContents(objSpecial->DirectoryPath);
} private:
static void ShowDirectoryContents(String *path)
// Строка {
Directorylnfo *dir = new Directorylnfo(path);
// путь
Filelnfo *files[] = dir->GetFiles();
Console::WriteLine("Files:");
// Файлы:
lEnumerator *pEnum = files->GetEnumerator();
// файлы
while (pEnum->MoveNext())
{
Filelnfo *f =
dynamic_cast(pEnum->Current);
Console::WriteLine(" {0}", f->Name);
// Имя
}
Directorylnfo *dirs [] = dir->GetDirectories(
};
Console::WriteLine("Directories:");
// Каталоги: pEnum = dirs->GetEnumerator();
while (pEnum->MoveNext()
}
{
Directorylnfo *d =
dynamic_cast(pEnum->Current);
Console::WriteLine(" {0}", d->Name);
// Имя
}
}
};

Вот выдача:

path = c:\OI\NetCpp\Chap08\CustomAttribute // путь Files: // Файлы
CustomAttribute.vcproj
CustomAttribute.neb
ReadMe.txt
CustomAttribute.cpp
Assemblylnfо.cpp
stdafx.cpp
stdafx.h
CustomAttribute.sin
CustomAttribute.suo
AttributeDemo.h
DirectoryContext.h
DirectoryAttribute.h Directories: // Каталоги
Debug // Отладка path = C:\OI\NetCpp\Chap08 // путь Files: // Файлы Directories: // Каталоги
Reflection // Отражение
Dynamic // Динамический
Filel()
Serialization // Преобразование в последовательную форму
Hotel // Гостиница
ISerialization
Threading
PulseAll
Threadlsolation
AppDomain
Asynch
AsynchThreading
CustomAttribute
MarshalByReference
Remoting

Чтобы создать пользовательский атрибут, необходимо определить класс атрибута, производный от базового класса Attribute (Атрибут). В соответствии с соглашением, нужно дать классу имя, заканчивающееся на "Attribute" ("Атрибут"). Имя класса без суффикса "Attribute" ("Атрибут") будет названием пользовательского атрибута. В нашем примере имя класса — InitialDirectoryAttribute, поэтому название атрибута— Initial-Directory.

Можно реализовать один или несколько конструкторов для класса атрибута. Конструкторы определяют, как передать позиционные параметры для атрибута (предоставив список параметров, разделенных запятыми). Возможно также предусмотреть "поименованные параметры" для пользовательского атрибута, чтобы при передаче информации через параметр можно было использовать синтаксис имя=значение.

Можно также предусмотреть свойства для чтения информации, передаваемой через параметр. В нашем примере есть свойство Path (Путь), которое инициализируется в конструкторе.

//DirectoryAttribute.h
using namespace System;
// использование пространства имен Система;
public _gc class InitialDirectoryAttribute :
// класс сборщика мусора InitialDirectoryAttribute:
public Attribute
// общедоступный Атрибут
{
private:
// частный
String *path;
// Строка public:
InitialDirectoryAttribute(String *path)
// Строка
{
this->path = path;
// путь
}'
_property String *get_Path()
// Строка свойства
{
return path;
// путь
}
};

Последний шаг при работе с самостоятельно создаваемыми атрибутами состоит в том, чтобы предусмотреть способ извлечения пользовательской информации об атрибуте из метаданных при помощи классов отражения. Можно получить Туре (Тип) любого объекта, вызывая метод GetType, который предоставляется корневым классом Object (Объект). Метод GetCustomAttributes этого класса позволяет прочитать пользовательскую информацию об атрибуте.

Чтобы упростить создание клиентской программы, часто полезно создать базовый класс, который будет проделывать работу по чтению пользовательской информации об атрибутах30. Мы создадим базовый класс DirectoryContext, который будет использоваться классом, желающим воспользоваться преимуществом атрибута Initial-Directory. Этот базовый класс предоставляет свойство DirectoryPath, чтобы возвратить информацию о пути, хранящуюся в метаданных. Вот листинг базового класса:

//DirectoryContext.h
using namespace System;
// использование пространства имен Система;
using namespace System::Reflection;
// использование пространства имен Система::Отражение;
using namespace System::10;
// использование пространства имен Система::10;
using namespace System:Collections;
использование пространства имен Система::Коллекции;
_gc class DirectoryContext
// класс сборщика мусора DirectoryContext
{
public:
_property String *get_DirectoryPath()
// Строка свойства
{
Type *t = this->GetType();
lEnumerator *pEnum =
t->GetCustomAttributes(true)->GetEnumerator();
while (pEnum->MoveNext())
{
Attribute *a =
dynamic_cast(pEnum->Current);
// Атрибут InitialDirectoryAttribute *da =
dynamic_cast(a);
if (da != 0)
// если (da ! = 0)
{
return da->Path;
// Путь
}
}
return Directory::GetCurrentDirectory();
// Каталог
}
};

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

Нужно импортировать пространство имен System:: Reflection (Система-Отражение). GetType возвращает текущий объект Туре (Тип), и тогда можно использовать метод GetCustomAttributes, чтобы получить коллекцию объектов Attribute (Атрибут) из метаданных. Так как эта коллекция неоднородна, поскольку состоит из различных типов, используется оператор dynamic_cast, чтобы проверить, принадлежит ли данный элемент коллекции к типу InitialDirectoryAttribute. Если такой элемент найдется, возвращается свойство Path (Путь). В противном случае возвращается заданный по умолчанию текущий каталог, который можно получить из GetCurrentDirectory.

Распределенная управляемая память автоматически возвращается системе с помощью алгоритма сборки мусора. Общеязыковая среда времени выполнения CLR отслеживает использование памяти, которая выделяется в управляемой ею динамически распределяемой области памяти, и любой участок памяти, на который больше ничто не ссылается, отмечается как "мусор". Когда памяти не хватает, общеязыковая среда времени выполнения CLR просматривает свои структуры данных, с помощью которых она отслеживает использование памяти, и возвращает всю память, помеченную как мусор. Таким образом, программист освобождается от ответственности за освобождение памяти.

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

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

Исключая ситуацию явного использования оператора delete (уничтожить), для управляемого объекта время разрушения недетерминировано. Деструктор для конкретного объекта, на который нет ссылок, может вызваться в любое время, когда выполняется процесс сборки мусора. Порядок вызовов деструкторов для различных объектов непредсказуем. Более того, при исключительных обстоятельствах деструктор может не вызваться совсем (например, поток войдет в бесконечный цикл или процесс завершится аварийно и тогда во время выполнения не будет возможности провести очистку). К тому же, если оператор delete (уничтожить) не используется явно, то поток, из которого будет вызван деструктор, не определен.

В примере ExplicitDelete демонстрируется, что вызов деструктора является синхронным, и поэтому детерминированным, если явно удалить управляемый указатель. Следующий код демонстрирует создание двух объектов. Первый завершается пассивно путем обнуления указателя на него. Сборщик мусора асинхронно вызывает деструктор в своем собственном потоке. Второй объект разрушается явно с помощью оператора delete (уничтожить) и деструктор вызывается синхронно. Программа с помощью хэш-кодов отображает подробности того, что случается с каждым объектом, и в каком потоке это случается. Из вывода можно увидеть, что в случае пассивно завершаемого объекта деструктор выполняется в потоке, отличном от того, в котором выполняется функция Main (Главная). И напротив, в случае явного уничтожения объекта деструктор выполняется в том же самом потоке, что и метод Main (Главный).

//ExplicitDelete.h
using namespace System::Threading;
// использование пространства имен
// Система::Организация поточной обработки;
public _gc class SomeClass
// класс сборщика мусора SomeClass
{
public:
-SomeClass()
{
Console::Write ( // Запись
"Destructor running in thread: {0}, ",
// "Деструктор, выполняющийся в потоке: (0), ",
_box(Thread::CurrentThread->GetHashCode()));
// Поток Console::WriteLine(
"Destroying object: {0}",
// "Уничтожение объекта: {0} ",
_box(this->GetHashCode()));
}
};
public _gc class ExplicitDelete
// класс сборщика мусора ExplicitDelete
{
public:
static void Main()
{
Console::WriteLine(
"Main thread: {0}",
// "Основной поток: {0} ",
_box(Thread::CurrentThread->GetHashCode()));
// Поток
SomeClass *sc = new SomeClass;
Console::WriteLine(
"Main thread creating object: {0}",
// "Основной поток, создающий объект: {0} ",
_box(sc->GetHashCode()));
Console::WriteLine(
"Nulling pointer to object: {0}",
// "Обнуление указателя на объект: {0} ",
_box(sc->GetHashCode()));
sc = 0;
GC::Collect (); // СБОРЩИК МУСОРА:: Собрать();
GC::WaitForPendingFinalizers(); // СБОРЩИК МУСОРА
sc = new SomeClass;
Console::WriteLine(
"Main thread creating object: {0}",
// "Основной поток, создавший объект: {0} ",
_box(sc->GetHashCode()));
Console::WriteLine(
"Deleting pointer to object: {0}",
// "Удаление указателя на объект: {0} ",
_box(sc->GetHashCode()));
delete sc; // удалить
Console::WriteLine("All done."); // Все сделано
}
};

Ниже приведена выдача.

Main thread: 2
Main thread creating object: 5
Nulling pointer to object: 5
Destructor running in thread: 6,
Destroying object: 5
Main thread creating object: 7
Deleting pointer to object: 7
Destructor running in thread: 2,
Destroying object: 7
All done.

Перевод такой:

Основной поток: 2
Основной поток, создавший объект: 5
Обнуление указателя на объект: 5
Деструктор, выполняющийся в потоке: 6,
Уничтожаемый объект: 5
Основной поток, создавший объект: 7
Удаление указателя на объект: 7
Деструктор, выполняющийся в потоке: 2,
Уничтожаемый объект: 7
Все сделано.

Чтобы избежать лишних накладных расходов, не стоит определять деструктор для класса, если на то нет серьезных причин. В случае, если деструктор все же реализуется, нужно, вероятно, в классе предоставить альтернативный, детерминированный механизм для выполнения необходимой очистки. Каркас .NET Framework рекомендует использовать шаблон проектирования Dispose (Освободить ранее выделенную область памяти) для детерминированной очистки, которую мы сейчас и рассмотрим.

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

public _gc _interface IDisposable
// сборщик мусора - интерфейс IDisposable
{
void Dispose();
};

В этом шаблоне проектирования определяется, что клиентская программа должна вызывать Dispose (Освободить ранее выделенную область памяти) для объекта, когда исчезает необходимость в этом объекте. Метод Dispose (Освободить ранее выделенную область памяти) реализуется так, что класс выполняет все необходимые действия по очистке. Для полной гарантии, в классе нужно реализовать и деструктор— на случай, если метод Dispose (Освободить ранее выделенную область памяти) никогда не будет вызван, возможно, из-за появления исключения33. Так как и метод Dispose (Освободить ранее выделенную область памяти), и деструктор производят очистку, код, освобождающий ресурсы, может быть помещен в Dispose (Освободить ранее выделенную область памяти), а в деструкторе можно просто вызывать Dispose (Освободить ранее выделенную область памяти). Метод Dispose (Освободить ранее выделенную область памяти) разработан так, что клиентская программа может вызывать его при завершении работы с объектом или когда будет известно, что нет никакой опасности в освобождении ресурсов, связанных с объектом.

Отметим, что недопустимо вызывать деструктор объекта после вызова метода Dispose (Освободить ранее выделенную область памяти), потому что это приведет к повторной очистке. Объект может быть удален из очереди сборки мусора с помощью GC: : SuppressFinalize. Кроме того, полезно реализовать в классе булев флажок, назвав его, например disposeCalled. Если Dispose (Освободить ранее выделенную область памяти) вызывается дважды, проверка этого флажка предотвратит повторное выполнение очистки.

Метод Dispose (Освободить ранее выделенную область памяти) должен также вызвать метод Dispose (Освободить ранее выделенную область памяти) базового класса с целью удостовериться, что все его ресурсы тоже освобождаются. Причем и этот метод должен быть написан так, чтобы не возникала исключительная ситуация при его вызове, если ресурсы уже были освобождены.

Поскольку завершение представляет собой дорогой процесс, любой объект, который больше не будет использовать ресурсы, должен вызвать статический метод GC:: SupressFinalize, передавая ему указатель this. При наличии в коде блока try/finally можно разместить вызов метода Dispose (Освободить ранее выделенную область памяти) объекта в блоке finally (наконец), чтобы удостовериться в том, что ресурсы будут освобождены.

Пример программы DisposeDemo иллюстрирует шаблон для освобождения ресурсов. Класс SimpleLog с помощью класса StreamWriter реализует запись (информации) в файл.

//SimpleLog.h
using namespace System;
// использование пространства имен Система;
using namespace System::10;
// использование пространства имен Система::10;
public _gc class SimpleLog :
public IDisposable
// класс сборщика мусора
SimpleLog: IDisposable
{
private: // частный
StreamWriter *writer; String *name;
// Строка bool disposeCalled;
// логический (булев) флажок disposeCalled public:
SimpleLog(String *fileName) : disposeCalled(false)
// (Строка *fileName): (ложь)
{
name = fileName; // имя файла
writer = new StreamWriter(fileName, false);
// устройство записи = новый StreamWriter (имя
// файла, ложь);
writer->AutoFlush = true;
// устройство записи-> Автосброс = истина;
Console::WriteLine(
String::Format("logfile {0} created", name));
// Строка::Формат ("системный журнал (0}
// создан", имя));
}
void WriteLine(String *str) // Строка
{
writer->WriteLine(str); // запись
Console::WriteLine(str);
}
void Dispose ()
{
if(disposeCalled)
// если
(disposeCalled) - если все уже сделано
return;
writer->Close ();
GC::SuppressFinalize (this);
// СБОРЩИК МУСОРА Console::WriteLine(
String::Format("logfile {0} disposed", name));
// Строка::Формат ("системный журнал {0}
// закрыт ", имя));
disposeCalled = true;
// истина - все уже сделано
}
-SimpleLog()
{
Console::WriteLine(
String::Format("logfile {0} finalized", name));
// Строка::Формат ("системный журнал (0)
// завершен", имя));
Dispose();
}
};

Класс SimpleLog поддерживает интерфейс IDisposable и таким образом реализует метод Dispose (Освободить ранее выделенную область памяти). Код, освобождающий ресурсы, просто удаляет объект streamWriter. Чтобы удостовериться в том, что для удаленного объекта также не будет вызван метод завершения, вызывается GC: : SuppressFinalize. Завершающий работу объекта деструктор просто делегирует свои функции методу Dispose (Освободить ранее выделенную область памяти). Для контроля над продолжительностью жизни объекта на консоль выводится сообщение из конструктора, из метода Dispose (Освободить ранее выделенную область памяти) и из деструктора.

Вот код тестовой программы:

//DisposeDemo.h
using namespace System;
// использование пространства имен Система;
using namespace System::Threading;
// использование пространства имен
// Система::Организация поточной обработки;
public _gc class DisposeDemo
// класс сборщика мусора DisposeDemo
{ public:
static void Main()
{
SimpleLog *log = new SimpleLog("logl.txt");
log->WriteLine("First line");
// файл регистрации-> WriteLine ("Первая строка");
Pause(); // Пауза
log->Dispose(); // Первое завершение файла регистрации
log->Dispose(); // файл регистрации - испытание -второй
// вызов Dispose
log = new SimpleLog("Iog2.txt");
log->WriteLine("Second line");
// файл регистрации-> WriteLine ("Вторая строка");
Pause (); // Пауза log = new SimpleLog(
"Iog3.txt"); // предыдущий (2-ой) файл
// регистрации освобожден
log->WriteLine("Third line");
// файл регистрации-> WriteLine ("Третья строка");
Pause (); // Пауза
log =0; // последний файл регистрации освобожден
GC::Collect();
// СБОРЩИК МУСОРА:: Собрать ( );
Thread::Sleep(100);
// Поток:: Бездействие (100);
}
private: // частный
static void Pause)) // Пауза
{
Console::Write("Press enter to continue");
// Запись:: ("Нажмите ввод для продолжения");
String *str = Console::ReadLine(); // Строка
}
};

Указателю log (файл регистрации) на объект SimpleLog по очереди присваиваются три различных указателя на экземпляр объекта. Первый раз ранее выделенная область памяти освобождается должным образом. Второй раз указателю присваивается указатель на третий объект. Это происходит до освобождения ранее выделенной области памяти для второго объекта. В результате второй объект становится мусором. Используя метод Pause (Пауза) для приостановки выполнения этого консольного приложения, мы можем исследовать состояние файлов logl. txt, Iog2 . txt и Iog3. txt в разные моменты выполнения программы.

Выполнение программы приводит к следующему выводу:

logfile logl.txt created First line
Press enter to continue logfile logl.txt
disposed logfile Iog2.txt created Second line
Press enter to continue logfile Iog3.txt created Third line
Press enter to continue logfile Iog3.txt
finalized logfile Iog3.txt
disposed logfile Iog2.txt
finalized logfile Iog2.txt disposed

Перевод такой:

системный журнал logl.txt создан
Первая строка
Нажмите ввод для продолжения
системный журнал logl.txt завершен
системный журнал Iog2.txt создан
Вторая строка
Нажмите ввод для продолжения
системный журнал Iog3.txt создан
Третья строка
Нажмите ввод для продолжения
системный журнал Iog3.txt завершен
системный журнал Iog3.txt освобожден
системный журнал Iog2.txt завершен
системный журнал Iog2.txt освобожден

После первой паузы файл logl. txt уже создан, и мы можем просмотреть его содержимое с помощью стандартной системной программы Блокнот (Notepad). Если попробовать удалить файл, получим нарушение процедуры совместного использования (общих ресурсов), т.е. нарушение условий коллективного доступа (ошибка совместного доступа, т.к. файл уже открыт другим приложением), как показано на рис. 8.2.

После второй паузы ранее выделенная для logl. txt область памяти будет освобождена и его можно будет удалить. Файл Iog2.txt к этому моменту уже был создан и открыт. А до третьей паузы будет создан Iog3.txt. Но объектной ссылке на 1од2 . txt было присвоено новое значение, поэтому теперь клиентская программа не может освободить область памяти, ранее выделенную для второго объекта. Если бы Dispose (Освободить ранее выделенную область памяти) был единственным механизмом для удаления второго объекта, нам бы не повезло. К счастью, в классе SimpleObject реализован деструктор, так что при следующей сборке мусора удастся избавиться от второго объекта должным образом. Можно увидеть результат завершения, выполнив программу до конца. Второй объект действительно завершается и ранее выделенная для него область памяти освобождается. Фактически при завершении работы прикладной области деструкторы вызываются для всех не уничтоженных объектов, даже для объектов, которые все еще являются доступными.

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

В нашем коде мы явно делаем третий объект недоступным, присваивая указателю пустой указатель (log = null), и принудительно устраиваем сборку мусора, вызывая GC: : Collect (СБОРЩИК МУСОРА::Собрать). Потом приложение на некоторое время переходит в режим . ожидания, чтобы сборщик мусора выполнил свою работу до конца перед завершением прикладной области. Наша тестовая программа несколько искусственна, так как сборка мусора недетерминирована. Сборщик мусора вызывается автоматически при выходе из программы и завершении работы прикладной области. Однако при этом системные объекты, такие как Console (Консоль), также закрываются. Так как нельзя полагаться на порядок завершения, можно получить исключение при вызове WriteLine внутри деструктора. Явный вызов GC: :Collect (СБОРЩИК МУСОРА::Собрать) вызывает принудительную сборку мусора в то время, когда системные объекты все еще открыты. Если мы опустим последние три строки из метода Мат (Главный), то можем получить идентичный вывод, но можем также получить и исключение.

Дополнительное имя для Dispose (Освободить ранее выделенную область памяти)

Стандартное имя метода, производящего очистку— Dispose (Освободить ранее выделенную область памяти). Соглашение состоит в том, что, как только ранее выделенная для объекта область памяти освобождается, такой объект завершает свою работу. Однако в некоторых случаях, например для файла, тот же самый экземпляр объекта мог бы использоваться повторно. Файл может быть открыт, закрыт, а затем снова открыт. При этом стандартное соглашение об именовании гласит, что метод для очистки должен называться Close (Закрыть). В других случаях может использоваться другое подходящее имя.

Чтобы вполне естественно, если бы в нашем классе SimpleLog имелся метод Open (Открыть). В этом случае метод для очистки логично было бы назвать Close (Закрыть). Простоты ради, мы не реализовали метод Open (Открыть), и поэтому придерживались имени Dispose (Освободить ранее выделенную область памяти).

Чтобы оптимизировать технологию, каждому объекту, находящемуся в управляемой динамически распределяемой области памяти, назначается поколение. Так, новому объекту назначается поколение 0, и такой объект рассматривается в качестве главного кандидата для сборки мусора. Ранее созданные объекты назначаются поколению 1. Поскольку такие объекты уже просуществовали некоторое время, есть вероятность, что их время жизни будет более продолжительным, чем у объектов поколения 0. Еще более старым объектам назначается поколение 2. Считается, что вероятность пережить сборку мусора у них еще больше. Максимальный номер поколения в текущей реализации .NET равен 2. Это число может быть взято из свойства GC : : MaxGeneration.

Обычно сборщик мусора "выметает" только поколение 0. Именно в этом поколении находятся наиболее вероятные кандидаты на освобождение памяти. Все объекты поколения 0, пережившие сборку мусора, переходят в поколение 1. Если освобожденной памяти недостаточно, будут "выметены" объекты поколения 1, а выжившие будут продвинуты в поколение 2. Ну а затем, в случае необходимости, будут "выметены" и объекты поколения 2 и так далее вплоть до MaxGeneration — максимального количества поколений.

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

//FinalizeStackUnwind.h
using namespace System;
// использование пространства имен Система;
using namespace System::Threading;
// использование пространства имен
// Система::Организация поточной обработки;
public _gc class FinalizeStackUnwind
// класс сборщика мусора FinalizeStackUnwind
{
public:
static void Main()
{
try
{
SomeMethod(); }
catch(Exception *e)
// Исключение
{
Console::WriteLine(e->Message);
// Сообщение
}
GC::Collect( ) ;
// СБОРЩИК МУСОРА:: Собрать()
Thread::Sleep(100); // Поток:: Бездействие
}
private: // частный
static void SomeMethod ()
{
// локальная переменная
SimpleLog *alpha = new SimpleLog("alpha.txt");
// вызвать исключение
throw new Exception("error!!");
// новое Исключение ("ошибка!!");
}
};

SomeMethod выделяет место для локальной переменной-указателя alpha (алфавитный) типа SimpleLog*. Перед нормальным завершением метода, из него вызывается исключение. Механизм раскручивания стека при обработке особых ситуаций видит, что переменная-указатель alpha (алфавитный) больше не доступна, и помечает ее для сборки мусора. Вызов GC: :Collect (СБОРЩИК МУСОРА::Собрать) приводит в действие сборку мусора, и мы видим в выдаче программы, что завершение действительно выполнено.

logfile alpha.txt created
error!!
logfile alpha.txt finalized
logfile alpha.txt disposed

Перевод такой:

системный журнал alpha.txt создан
ошибка!!
системный журнал alpha.txt завершен
системный журнал alpha.txt освобожден

Обычно лучше всего не вмешиваться в работу сборщика мусора, — пусть он выполняет свою работу незаметно для вас. Иногда, однако, программе может быть выгодно вмешаться в его работу. Пространство имен System (Система) содержит класс GC (СБОРЩИК МУСОРА), который дает возможность программе изменить поведение сборщика мусора. Мы рассмотрим несколько важных методов класса GC (СБОРЩИК МУСОРА).

SuppressFinalize

Данный метод производит запрос к системе, чтобы та не выполняла операцию завершения, т.е. не вызвала деструктор указанного объекта. Как мы видели раньше, нужно вызвать этот метод в собственной реализации метода Dispose (Освободить ранее выделенную область памяти), чтобы предотвратить выполнение операции завершения объекта, если ранее выделенная для него область памяти уже освобождена.

Collect (Собрать)

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

MaxCeneration

Это свойство возвращает максимальное поддерживаемое общеязыковой средой времени выполнения CLR число поколений.

CetCeneration

Данный метод возвращает текущий номер поколения, которое назначено объекту.

CetTotalMemory

Указанный метод возвращает объем распределенной в настоящее время памяти в байтах (а не объем свободной доступной памяти и не общий объем динамически распределяемой области памяти). Передаваемый параметр позволяет определять, должна ли система выполнить сборку мусора перед возвратом из метода. Если сборка мусора не производится, возвращаемое число байтов вероятно больше, чем фактический объем памяти, используемой объектами.

Программа GarbageCollection (Сборка мусора) иллюстрирует использование рассмотренных выше методов класса GC (СБОРЩИК МУСОРА). Пример несколько искусственен. С его помощью просто иллюстрируется продолжительность жизни объекта и эффект использования различных методов класса GC (СБОРЩИК МУСОРА). Объекты, для которых выделяется память, принадлежат классу Member (Элемент). Этот класс имеет свойство типа String (Строка) под названием Name (Имя). Операторы вывода использованы в конструкторе, деструкторе и методе Dispose (Освободить ранее выделенную область памяти). Класс Committee (Комитет) поддерживает список массивов, состоящий из экземпляров класса Member (Элемент). Метод RemoveMember просто удаляет элемент из списка массивов. Метод DisposeMember также вызывает метод Dispose (Освободить ранее выделенную область памяти) для вычеркиваемого из списка элемента. Метод ShowGenerations отображает номер поколения каждого объекта класса Member (Элемент). Испытательная программа GarbageCollection.h использует эти классы; она показывает результаты различных размещений (распределений) и освобождений памяти при использовании методов класса GC (СБОРЩИК МУСОРА). Код и вывод должны быть весьма просты для понимания.

Вся память распределяется локально в методе DemonstrateGenerations. После того, как указанный метод завершит свою работу, и его локальная память стает недоступной, мы явно вызываем GC: :Collect (СБОРЩИК МУСОРА: :Собрать). Это приводит к вызову соответствующих деструкторов, прежде чем вся прикладная область прекратит свое существование. Таким образом мы избегаем возможного случайного исключения в закрывающемся потоке, когда метод WriteLine вызывается в методе завершения. Этот самый прием мы уже видели в более ранних примерах.

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

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

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

.NET упрощает программирование управления памятью с помощью эффективного, автоматического средства сборки мусора, которое учитывает поколения объектов. Процесс завершения объекта недетерминирован, но можно реализовать детерминированный процесс очистки с помощью шаблона освобождения памяти dispose (Освободить ранее выделенную область памяти) или явного использования оператора delete (уничтожить).

назад главная вперед

Главная Новости Visual Studio .NET Самоучитель C# Учебники О сайте
Framework 3.0
Microsoft официально заявила о переименовании своего API WinFX в более привычный для слуха .NET Framework 3. Это, вероятно, последнее переименование. Нет уже Longhorn (Vista), канули в прошлое Avalon (WPF), Indigo (WCF). Время пришло переименовать WinFX…
DirectX 10
Новый набор интерфейсов программирования приложений (API) DirectX 10 увеличит скорость работы компьютеров при запуске ряда приложений в 4 раза. Об этом заявили разработчики DirectX компания Microsoft, сообщается на сайте The Inquirer...
Microsoft в России
Компания Microsoft открыла производство в России, пишет газета "Ведомости". Диски с Windows и Microsoft Office с марта печатаются на ярославском полиграфическом комбинате ООО "Сонопресс". Sonopress входит в концерн Bertelsmann и производит диски также для Sony BMG...
счетчик посещений
Используются технологии uCoz