- •Сборки (assembly) в среде .Net. Проблема версионности сборок и ее решение.
- •Номер версии в .Net
- •Сведения о версии
- •Номер версии сборки
- •Информационная версия сборки
- •Общая система типов данных в среде .Net. Размерные и ссылочные типы данных. Типы, переменные и значения
- •Пользовательские типы
- •Система общих типов cts
- •Ссылочные типы
- •Типы литеральных значений
- •Неявные типы, анонимные типы и типы, допускающие значение null
- •Упаковка и распаковка размерных типов данных в среде .Net.
- •Производительность
- •Упаковка–преобразование
- •Распаковка-преобразование
- •Ссылочные типы данных. Объектная модель в среде .Net и языке c#.
- •Модели ручной и автоматической утилизации динамической памяти, их сравнительная характеристика. Модель с ручным освобождением памяти
- •Модель с автоматической «сборкой мусора»
- •Модель автоматической утилизации динамической памяти, основанная на сборке мусора. Проблема недетерминизма.
- •Модель автоматической утилизации динамической памяти, основанная на аппаратной поддержке (тегированной памяти).
- •Сборка мусора в среде .Net. Построение графа достижимых объектов.
- •Сборка мусора в среде .Net. Механизм поколений объектов.
- •Модель детерминированного освобождения ресурсов в среде .Net. Интерфейс iDisposable и его совместное использование с завершителем (методом Finalize).
- •«Мягкие ссылки» и кэширование данных в среде .Net.
- •Краткие и длинные слабые ссылки
- •Краткая ссылка
- •Длинная ссылка
- •Правила использования слабых ссылок
- •Динамические массивы в среде .Net и языке c#.
- •Приведение типов в массивах
- •Все массивы неявно реализуют /Enumerable, /Collection и iList
- •Передача и возврат массивов
- •Создание массивов с ненулевой нижней границей
- •Производительность доступа к массиву
- •Небезопасный доступ к массивам и массивы фиксированного размера
- •Делегаты в среде .Net и механизм их работы. Знакомство с делегатами
- •Использование делегатов для обратного вызова статических методов
- •Использование делегатов для обратного вызова экземплярных методов
- •Правда о делегатах
- •Использование делегатов для обратного вызова множественных методов (цепочки делегатов)
- •Поддержка цепочек делегатов в с#
- •Расширенное управление цепочкой делегатов
- •Упрощение синтаксиса работы с делегатами в с#
- •Упрощенный синтаксис № 1: не нужно создавать объект-делегат
- •Упрощенный синтаксис № 2: не нужно определять метод обратного вызова
- •Упрощенный синтаксис № 3: не нужно определять параметры метода обратного вызова
- •Упрощенный синтаксис № 4: не нужно вручную создавать обертку локальных переменных класса для передачи их в метод обратного вызова
- •Делегаты и отражение
- •События в среде .Net; реализация событий посредством делегатов. События
- •Этап 1: определение типа, который будет хранить всю дополнительную информацию, передаваемую получателям уведомления о событии
- •Этап 2: определение члена-события
- •Этап 3: определение метода, ответственного за уведомление зарегистрированных объектов о событии
- •Этап 4: определение метода, транслирующего входную информацию в желаемое событие
- •Как реализуются события
- •Создание типа, отслеживающего событие
- •События и безопасность потоков
- •Явное управление регистрацией событий
- •Конструирование типа с множеством событий
- •Исключительные ситуации и реакция на них в среде .Net. Достоинства
- •Механика обработки исключений
- •Блок try
- •Блок catch
- •Блок finally
- •Генерация исключений
- •Определение собственных классов исключений
- •Исключения в платформе .Net Framework
- •Исключения и традиционные методы обработки ошибок
- •Управление исключениями средой выполнения
- •Фильтрация исключений среды выполнения
- •21 Средства многопоточного программирования в среде .Net. Автономные потоки. Пул потоков.
- •Создание и использование потоков
- •Запуск и остановка потоков
- •Методы управления потоками
- •Безопасные точки
- •Свойства потока
- •Потоки Windows в clr
- •К вопросу об эффективном использовании потоков
- •Пул потоков в clr
- •Ограничение числа потоков в пуле
- •22. Асинхронные операции в среде .Net. Асинхронный вызов делегатов.
- •23. Синхронизация программных потоков в среде .Net. Блокировки.
- •Двойная блокировка
- •Класс ReaderWriterLock
- •Использование объектов ядра Windows в управляемом коде
- •Вызов метода при освобождении одного объекта ядра
- •24. Синхронизация программных потоков в среде .Net. Атомарные (Interlocked-операции). Семейство lnterlocked-методов
- •25. Прерывание программных потоков в среде .Net. Особенности исключительной ситуации класса ThreadAbortException.
- •26. Мониторы в среде .Net. Ожидание выполнения условий с помощью методов Wait и Pulse. Класс Monitor и блоки синхронизации
- •«Отличная» идея
- •Реализация «отличной» идеи
- •Использование класса Monitor для управления блоком синхронизации
- •Способ синхронизации, предлагаемый Microsoft
- •Упрощение кода c# при помощи оператора lock
- •Способ синхронизации статических членов, предлагаемый Microsoft
- •Почему же «отличная» идея оказалась такой неудачной
- •Целостность памяти, временный доступ к памяти и volatile-поля
- •Временная запись и чтение
- •Поддержка volatile-полей в с#
- •27. Асинхронный вызов делегатов.
- •Общие типы (Generics)
- •Инфраструктура обобщений
- •Открытые и закрытые типы
- •Обобщенные типы и наследование
- •Проблемы с идентификацией и тождеством обобщенных типов
- •«Распухание» кода
- •Обобщенные интерфейсы
- •Обобщенные делегаты
- •Обобщенные методы
- •Логический вывод обобщенных методов и типов
- •Обобщения и другие члены
- •Верификация и ограничения
- •Основные ограничения
- •Дополнительные ограничения
- •Ограничения конструктора
- •Другие вопросы верификации
- •Приведение переменной обобщенного типа
- •Присвоение переменной обобщенного типа значения по умолчанию
- •Сравнение переменной обобщенного типа с null
- •Сравнение двух переменных обобщенного типа
- •Использование переменных обобщенного типа в качестве операндов
- •Преимущества использования общих типов
- •29. Итераторы в среде .Net. Создание и использование итераторов.
- •Общие сведения о итераторах
Почему же «отличная» идея оказалась такой неудачной
Идея логически связать структуру данных синхронизации с каждым объектом кучи выглядит очень заманчивой, и это действительно так. Но разработчики Microsoft совершили большую ошибку при реализации этой идеи в CLR. Сейчас объясню почему.
Помните, неуправляемый код C++, приведенный в разделе «Отличная идея» в этой главе? Если бы вы писали этот код самостоятельно, сделали ли бы вы поле CRTICAL SECTION открытым? Конечно, нет! Открытое поле позволяет любому коду приложения изменять структуру CRTICAL_SECTION. Злоумышленнику ничего бы не стоило вызвать взаимную блокировку потоков, в которых работают экземпляры этого типа.
Ну и что же — блок синхронизации как раз имеет открытую структуру синхронизации данных, связанную с каждым объектом кучи! Любой код, имеющий ссылку на объект, в любой момент может передать эту ссылку в методы Enter и Exit и перехватить блокировку. Любой код может также передать в эти методы ссылку на любой объект-тип и перехватить блокировку этого типа. Проблема также возникает при передаче интернированного объекта String, поскольку теперь несколько строк использует одну блокировку. И, если передавать ссылку на объект производного от MarshalByRefObject типа, можно заблокировать либо сам объект, либо прокси-объект (сам объект останется не заблокированным).
Ниже приведен код, показывающий, к каким катастрофическим последствиям это может привести. В этом коде поток Main создает объект SomeType и налагает на этот объект блокировку. В какой-то момент времени происходит сборка мусора (в этом коде она принудительно инициируется только для демонстрации), и когда вызывается метод Finalize объекта SomeType, он пытается перехватить блокировку объекта. К сожалению, завершающий поток CLR не может получить блокировку объекта, так как ею владеет основной поток приложения. Это приводит к остановке CLR-потока деструктора — теперь ни один процесс (включая все AppDomain процесса) не сможет завершиться и никакая память, занятая объектами в куче, не может освободиться!
using System;
using System.Threading;
public sealed class SomeType
{
// Это метод Finalize объекта SomeType.
~SomeType()
{
// С целью демонстрации CLR-поток деструктора
// пытается перехватить блокировку объекта.
// ПРИМЕЧАНИЕ: поскольку блокировкой объекта владеет поток Main,
// завершающий поток блокируется!
Monitor.Enter(this);
}
}
public static class Program
{
public static void Main()
{
// Создание объекта
SomeType.SomeType st = new SomeType();
// Этот некорректный код перехватывает блокировку объекта
// и не освобождает ее.
Monitor.Enter(st);
// Для демонстрации запускаем сборку мусора
// и ожидаем завершения выполнения методов-деструкторов.
st = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("We never get here, both threads are deadlocked!");
}
}
К сожалению, CLR, FCL и компиляторы предлагают много функций, в которых используется блокировка объектов. В главе 10 я уже упоминал о возможных проблемах с событиями. CLR также использует открытую блокировку объекта типа при вызове конструктора класса для типа.
В пространстве имен SystemRuntime.CompilerServices есть особый класс атрибутов по имени MethodlmplAttribute. Этот атрибут можно применять к методу, устанавливая флаг MethodlmplOptions.Synchronized. Если это экземплярный метод, то при этом JIT-компилятор заключает весь код метода в функцию lock(this). Если это статический метод, код помещается в функцию lock(typeof(<имя_типа>)), где <имя_типа> — имя самого типа. Это плохо — о причинах уже говорилось ранее, поэтому никогда не следует использовать MethodlmplAttribute с флагом MethodlmplOptions.Synchronized.
Нам не удастся исправить CLR, FCL или компилятор C#, но при написании собственного кода, можно соблюдать максимальную осторожность и обходить проблему открытой блокировки. Для этого лишь нужно определить закрытое поле System.Object в качестве члена своего типа, создать объект, а затем передать ссылку на закрытый объект в оператор lock. Вот исправленная версия класса Transaction, которая более защищена за счет использования закрытой блокировки объекта:
internal sealed class TransactionWithLockObject
{
// Выделяем память для закрытого объекта, используемого для блокирования.
private Object m_lock = new Object();
// Поле, указывающее время выполнения последней транзакции.
private DateTime timeOfLastTransaction;
public void PerformTransaction()
{
lock (m_lock)
{
// Блокируем объект закрытого поля.
// Выполнение транзакции...
// Записываем время последней транзакции.
timeOfLastTransaction = DateTime.Now;
} // Отмена блокировки объекта закрытого поля.
// Неизменяемое свойство, возвращающее время последней транзакции.
}
public DateTime LastTransaction
{
get
{
lock (m_lock)
{
// Блокировка объекта закрытого поля.
// Возвращение даты и времени.
return timeOfLastTransaction;
}
// Отмена блокировки объекта закрытого поля.
}
}
}
Внимание! В этом коде показано, как избавляться от проблем, о которых говорилось ранее. Самая важная его особенность — блокировка синхронизации потока с использованием закрытого поля. Это не позволяет постороннему коду получить доступ к блокировке и, следовательно, вмешаться в код вашего типа и вызвать взаимную блокировку потоков.
Кажется странным создавать в классе Monitor объект System.Object только для синхронизации. В Microsoft спроектировали класс Monitor некорректно. Его нужно было спроектировать так, чтобы для создания блокировки синхронизации потока нужно было создать экземпляр типа Monitor. Затем статические методы должны быть экземплярными методами, самостоятельно работающими с объектом блокировки — тогда не пришлось бы передавать аргумент System.Object в методы класса Monitor. Это решило бы все проблемы и упростило бы модель программирования. Кстати, приведенный выше код легко модифицировать для синхронизации статических методов — достаточно просто изменить все члены на static. Если в вашем типе уже определены какие-либо закрытые поля данных, можете задействовать одно из них как объект блокировки, передаваемый в методы класса Monitor. Это поможет сэкономить немного памяти, поскольку не нужно будет размещать объект System.Object. Однако я бы не стал делать этого просто ради незначительной экономии памяти; код типов закрытых полей может вызывать метод lock(this). Если это случится, код будет заблокирован, и может возникнуть взаимная блокировка.
Внимание! Никогда не передавайте переменную значимого типа в метод Monitor.Enter или оператор lock языкаC#. У распакованных экземпляров значимого типа нет члена «индекс блокировки синхронизации», а поэтому их нельзя использовать для синхронизации. Если передать распакованный экземпляр значимого типа в метод Monitor.Enter, компиляторC# автоматически создаст код упаковки экземпляра. Если тот же экземпляр передать в Monitor.Exit, снова произойдет упаковка. В результате код заблокирует один, но разблокирует совершенно другой объект, и никакой безопасности потоков обеспечено не будет. Если передать распакованный экземпляр значимого типа оператору lock, компилятор обнаружит это и вернет ошибку:
error CSO185: Valuetype' is not a reference type as required by the lock statement (ошибка CSO185: значимый тип не является ссылочным типом, который требуется оператору lock).
Однако компилятор не вернет ошибку или предупреждение, если в метод Monitor.Enter или Monitor.Exit передать экземпляр значимого типа.
Pulse и Wait
Monitor.Wait- Освобождает блокировку объекта и блокирует текущий поток до тех пор, пока тот не получит блокировку снова.
Monitor.Pulse- уведомляет поток в очереди готовности об изменении состояния объекта с блокировкой. Только текущий владелец блокировки может послать сигнал ожидающему объекту, используя Pulse.
Поток, который в данный момент владеет блокировкой указанного объекта, вызывает этот метод для сообщения следующему в очереди потоку о блокировке. При получении импульса ожидающий поток перемещается в очередь готовности. Когда поток, вызвавший метод Pulse, освобождает блокировку, следующий поток в очереди готовности (который необязательно является потоком, получившим импульс) получает блокировку.
Класс Monitor не поддерживает состояние, указывающее, что метод Pulse не вызывался. Таким образом, если вызывается Pulse, когда нет потоков в ожидании, следующий поток вызывает блоки Wait, как если бы Pulse никогда не вызывались.
Если два потока используют Pulse и Wait, чтобы взаимодействовать, это может привести их к взаимоблокировке.
Синхронизированный объект имеет несколько ссылок, в том числе ссылку на поток, удерживающий блокировку в настоящее время, ссылку на очередь готовности, в которой находятся потоки, готовые к получению блокировки, и ссылку на очередь ожидания, в которой находятся потоки, ожидающие уведомления об изменении состояния объекта.
Методы Pulse, PulseAll и Wait должны быть вызваны из синхронизированного блока кода.
Для сигнализации нескольких потоков используется метод PulseAll.
========================================================================
Чтобы предотвратить повреждение ресурса по причине одновременного доступа многих потоков, нужно использовать конструкции синхронизации потоков.
При построении повторно используемой библиотеки классов нужно позаботиться, чтобы все статические методы типов обеспечивали безопасность потоков, то есть чтобы при одновременном вызове несколькими потоками статического метода типа состояние типа не повреждалось. Для этого нужно использовать одну из конструкций синхронизации потоков. Обеспечивать безопасность потоков при работе статических методов в многопоточной среде обязательно, потому что пользователи классов просто не в состоянии реализовать это в своем коде.
Представьте, что статический метод Load одновременно пытаются вызвать код библиотечной сборки этой сборки и другой код в исполняемой сборке. Обеим сборкам придется согласовать используемую конструкцию синхронизации потока, и они обе должны «уметь» обнаруживать ее.
С другой стороны, библиотека повторно используемых классов не обязана обеспечивать безопасность потоков во всех экземплярных методах типа. Причина в том, что обеспечение безопасности работы с потоками замедляет работу методов, а большинство объектов используется одним потоком; обеспечение безопасности потоками значительно ударит по производительности. Кроме того, когда код создает объект, ни у какого другого кода нет доступа к этому объекту, если только ссылка на него каким-то образом не передана в этот код. Любой код, передающий ссылку на объект, может также передавать информацию о конструкции синхронизации потоков, чтобы код, исполняемый другими потоками, мог обратиться к объекту, обеспечивая безопасность потоков.