- •Сборки (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. Создание и использование итераторов.
- •Общие сведения о итераторах
Целостность памяти, временный доступ к памяти и volatile-поля
Для повышения производительности при частых обращениях к памяти на современных процессорах предусмотрена встроенная кеш-память. Обращение к этой памяти выполняется чрезвычайно быстро, особенно по сравнению со скоростью доступа к памяти на материнской плате. Когда поток впервые считывает значение из памяти, процессор извлекает нужное значение из оперативной памяти и размещает в собственном встроенном кеше. В действительности, производительность дополнительно повышается за счет того, что за одно обращение процессор извлекает несколько расположенных рядом байт (это называют строкой кеша), поскольку приложение обычно считывает из памяти байты, расположенные рядом. Если один из расположенных рядом байт тоже считывается, он уже может быть в кеше; если это так, обращения к оперативной памяти не происходит и операция выполняется быстрее. Когда же поток выполняет запись в память, процессор просто обновляет соответствующий байт в кеше и не записывает обновленное значение в память на материнской плате. Это тоже способствует повышению производительности. В конечном итоге процессор сбросит все данные из кеша, но попозже.
Если приложение работает на однопроцессорном компьютере (на котором может быть установлен процессор Hyperthreading), разработчику приложения эта информация ни к чему, поскольку это никак не влияет на работу программы. Тем не менее, производительность повышается за счет использования кеш-памяти. Если на компьютере один процессор Hyperthreading, никакого видимого эффекта тоже не будет, так как два логических процессора используют один процессорный кеш. Однако, если приложение работает на многопроцессорной системе или системе с двухъядерным процессором, роль процессорного кеша может стать весьма заметной, и разработчикам следует принимать это во внимание при написании кода. Конечно, даже на таких процессорах разница заметна, только если несколько потоков обращается к одним и тем же байтам памяти. Если они обращаются к разным байтам, особой разницы не будет.
Вот пример:
internal sealed class CacheCoherencyProblem { private Byte uninitialized = 0; private Int32 m_value = 0;
// Этот метод выполняется одним потоком.
public void ThreadK) {
m_value = 5;
uninitialized =1;
// Этот метод выполняется другим потоком, public void Thread2() {
if (uninitialized == 1) {
// Эта строка может выполняться и отображать значение 0.
Console.WriteLine(m_value);
Теперь допустим, что экземпляр этого класса создается на компьютере с двумя процессорами. Первый процессор выполняет метод Thread 1, а второй — метод Tbread2. Представьте, что выполнение происходит следующим образом.
Поток второго процессора считывает из памяти байт, и этот байт располагается сразу перед байтами поля mjualue объекта. В этом случае считывается целая строка кеша, а значит, байты поля mjualue попадают в кеш второго процессора. С точки зрения программиста, процессор считал значение из этого поля прежде, чем программа запросила считывание этого поля.
Поток первого процессора выполняет метод Threadl, который присваивает полю m_value значение 5, но это изменение может произойти в кеше первого процессора. В конечном счете, это изменение будет записано в оперативную память. С точки зрения программиста, процессор записывает значение этого поля в память намного позже того момента, когда код запросил изменение поля.
Поток первого процессора продолжает выполнять метод Threadl и изменяет значение поля mjnitialized на 1. Это также происходит в кеше первого процессора, но байты поля mjnitialized находятся в другой строке кеша, и поэтому могут быть сброшены в оперативную память.
Поток второго процессора начинает выполнение метода Thread2, который сначала запрашивает значение поля mjnitialized. Поскольку' байт этого значения в кеше второго процессора нет, они считываются из оперативной памяти. Поле mjnitialized будет содержать значение 1, поэтому будет выполнено условие оператора if и его тело будет выполняться.
Поток второго процессора продолжает выполнять метод Thread2, который считывает значение поля m_value. Поскольку байты этого поля уже находятся в кеше второго процессора, они считываются оттуда, а не из оперативной памяти, и значение поля m_value считается равным 0!
Надеюсь, вы поняли, в чем проблема. Подведем итог: кеш процессоров улучшает производительность, но может привести к тому, что различные потоки будут использовать разные значения одной и той же переменной. Согласованность значений в кеше и оперативной памяти — серьезная проблема, и есть разные способы ее решения, но все они сильно снижают производительность. Лучше всего реализовать методы так, чтобы предотвратить одновременный доступ нескольких потоков к общим данным. Например, попытаться реализовать методы, которые обращаются только к параметрам и локальным переменным, чтобы другие потоки не имели доступа к этим переменным. Конечно, сама по себе переменная ссылочного типа обеспечивает безопасность потоков, но объект, на который она ссылается, — нет, поскольку иметь ссылку на него могут несколько потоков сразу. Более того, если несколько потоков обращаются к общим данным в режиме только для чтения, проблем не возникает. Проблема согласованности кеша существует только для общих данных, которые могут изменяться потоками.
Дополнительно ухудшает ситуацию то, что компиляторы С# и JIT при компиляции кода могут изменить порядок операторов и они могут выполняются не в том порядке, в каком были расположены в исходном коде. Да и сам процессор может вносить коррекцию в очередность выполнения машинных команд. Но при всем при этом компиляторы С# и JIT и процессор действуют так, чтобы все работало, как запланировал программист, но только с расчетом на один поток — когда несколько потоков обращается к общей памяти (как показано в предыдущем примере), выполнение операторов в не предусмотренном программистом порядке может создать проблемы.
Скорее всего, вы скажете, что никогда не видели, чтобы в вашей программе возникали проблемы из-за изменения порядка выполнения операторов, и это может оказаться чистой правдой. Это, без сомнения, так, если программа выполнялась на однопроцессорной системе или даже на компьютере с процессором Hyper-threading. Но в многопроцессорной системе или на компьютере с двухъядерным процессором неполадки такого рода вполне возможны. Кроме того, разные процессоры по-разному ведут себя в отношении согласованности кеша. Например, в процессорах х8б для обеспечения согласованности сделано многое. Поскольку архитектура хб4 обратно совместима с архитектурой х86, процессоры хб4 также обеспечивают согласованность кеша. Так что, если выполнять методы класса Cache-CoherencyProblem на компьютерах с процессорами х8б или хб4, проблем не будет. Метод Thread2 либо не вернет ничего, либо вернет значение 5.
Однако процессоры новой архитектуры (например, IA64) проектировались для полноценного использования того факта, что у каждого процессора собственный кеш, и для повышения производительности согласованность кеша практически не отслеживается. Процессоры 1Аб4 поддерживают обычные машинные команды для считывания значения из памяти в регистр и для записи значения регистра в память. Но есть и модифицированная версия команды чтения, которая считывает байты из памяти, а затем делает недействительным кеш процессора, чтобы следующая команда чтения извлекала значения из оперативной памяти. Это называют временным чтением (volatile read) или чтением с семантикой запроса (read with acquire semantics).
В архитектуре IA64 также есть видоизмененная версия команды записи, которая записывает байты из регистра в память и сбрасывает кеш процессора в основную память, так что остальные потоки, считывающие те же данные, получают самые последние значения. Это называют временной записью (volatile write) или записью с семантикой освобождения (write with release semantics).
В дополнение, в архитектуре IA64 предусмотрена команда защиты памяти (memory fence), которая сбрасывает кеш процессора в основную память, а затем объявляет его недействительным.