КАТЕГОРИИ:
АстрономияБиологияГеографияДругие языкиДругоеИнформатикаИсторияКультураЛитератураЛогикаМатематикаМедицинаМеханикаОбразованиеОхрана трудаПедагогикаПолитикаПравоПсихологияРиторикаСоциологияСпортСтроительствоТехнологияФизикаФилософияФинансыХимияЧерчениеЭкологияЭкономикаЭлектроника
|
Временная запись и чтениеТеперь мы знаем, как процессорный кеш влияет на поведение приложений и что некоторые процессоры предлагают особые команды, предоставляющие программисту определенный контроль над согласованностью кеша. Познакомимся, как CLR позволяет задействовать преимущества этих команд. Ясно, что одна из основных задач CLR — предоставление абстрактной виртуальной машины, чтобы программисту не приходилось писать код, рассчитанный на какую-либо конкретную архитектуру процессора. Наиболее распространенный способ абстрагироваться от реализации для конкретного процессора — предоставить соответствующий метод. Класс System.Threading.Thread предлагает несколько статических методов, которые выглядят примерно так:
static Object VolatileRead(ref Object address); static Byte VolatileRead(ref Byte address); static SByte VolatileRead(ref SByte address); static Int16 VolatileRead(ref Int16 address); static UInt16 VolatileRead(ref UInt16 address); static Int32 VolatileRead(ref Int32 address); static UInt32 VolatileRead(ref UInt32 address); static Int64 VolatileRead(ref Int64 address); static UInt64 VolatileRead(ref UInt64 address); static IntPtr VolatileRead(ref IntPtr address); static UlntPtr VolatileRead(ref UIntPtr address); static Single VolatileRead(ref Single address); static Double VolatileRead(ref Double address);
static void VolatileWrite(ref Object address, Object value); static void VolatileWrite(ref Byte address, Byte value); static void VolatileWrite(ref SByte address, SByte value); static void VolatileWrite(ref Int16 address, Int16 value); static void VolatileWrite(ref UInt16 address, UInt16 value); static void VolatileWrite(ref Int32 address, Int32 value); static void VolatileWrite(ref UInt32 address, UInt32 value); static void VolatileWrite(ref Int64 address, Int64 value); static void VolatileWrite(ref UInt64 address, UInt64 value); static void VolatileWrite(ref IntPtr address, IntPtr value); static void VolatileWrite(ref UIntPtr address, UlntPtr value); static void VolatileWrite(ref Single address, float value); static void VolatileWrite(ref Double address, Double value); static void MemoryBarrier();
Все методы VolatileRead выполняют чтение с семантикой запроса; они считывают значение, на которое ссылается параметр address, а затем объявляют недействительным кеш процессора. Все методы VolatileWrite выполняют запись с семантикой освобождения; они сбрасывают содержимое кеша в основную память, а затем изменяют значение, на которое ссылается параметр address, на значение аргумента value. Метод MemoryBarrier обеспечивает защиту памяти — он сбрасывает данные процессорного кеша в основную память, после чего объявляет кеш недействительным. Теперь мы можем на основе кода класса CacheCoherencyProblem создать новый класс VolatileMethod: internal sealed class VolatileMethod { private Byte uninitialized = 0; private Int32 m_value = 0; // Этот метод выполняется одним потоком, public void Thread"!() { m_value = 5; Thread.VolatileWrite(ref uninitialized, 1); // Этот метод выполняется другим потоком, public void Thread2() { if (Thread.VolatileRead(ref uninitialized) == 1) { // Если выполняется эта строка, отобразится значение 5. Console.WriteLine(m_value);
Внимание! Если для взаимодействия друг с другом потоки используют общую память, последний байт должен записываться в общую память с использованием временной записи. Считывание должно выполняться с использованием временного чтения. Приведенный выше код демонстрирует это.
Поддержка volatile-полей в С# Сложно сделать так, чтобы все программисты вызывали методы VolatileRead и VolatileWrite корректно. Разработчикам сложно помнить обо всем этом и прогнозировать, что могут делать с общими данными другие потоки, работающие в фоновом режиме. Компилятор C# предлагает ключевое слово volatile, которое можно применять к статическим полям или экземплярам полей следующих типов: Byte, SByte,Intl6, UIntl6,Jnt32, UInt32, Char, Single или Boolean. Это ключевое слово можно также использовать для ссылки на типы и любые поля перечислений, если в основе перечислимого типа лежит тип Byte, SByte, Intl6, UIntl6, Int32, UInt32, Single или Boolean. Компилятор JIT гарантирует, что все обращения к volatile-полям выполняются по механизму временного считывания и записи, так что не обязательно явно вызывать какие-либо статические методы VolatileXxx. Кроме того, ключевое слово volatile сообщает компиляторам С# и JIT, что поле не нужно кешировать в регистре процессора, в этом случае во всех операциях с этим полем будет использоваться значение, считанное из основной памяти. Ключевое слово volatile позволяет на основе кода класса VolatileMethod создать новый класс VolatileField: internal sealed class VolatileField { private volatile Byte uninitialized = 0; private Int32 m_value = 0; // Этот метод выполняется одним потоком, public void Thread1() { m_value =5; uninitialized = 1; // Этот метод выполняется другим потоком, public void Thread2() { if (uninitialized == 1) { // Если выполняется эта строка, отобразится значение 5. Console.WriteLine(m_value); } Некоторым разработчикам на С# (и мне в том числе) не нравится ключевое слово volatile — они считают, что в языке программирования этого слова быть не должно. Они полагают, что в большинстве алгоритмов требуется совсем немного операций временного чтения или записи в поле, а большинство обращений можно выполнить обычным путем, что, к тому же, положительно скажется на производительности. Редко требуется, чтобы все операции доступа к полю были временными. Например, сложно сказать, как выполнять операции временного считывания в алгоритмах, подобных этому: m_amount = m_amount + m_amount; // m_amount - определенное в классе поле. m_amount *= m_amount Кроме того, С# не поддерживает передачу volatile-полей в метод по ссылке. Например, если поле m_amount определено как volatile Int32, при попытке вызвать метод TryParse типа Int32 компилятор выдаст предупреждение: Boolean success = Int32.TryParse("123", out m_amount); // При обработке этой строки кода компилятор С# покажет следующее предупреждение: // CS0420: a reference to a volatile field will not be treated as volatile // (CS0420: ссылка на volatile-поле не будет считаться временной). Создавая JIT-компилятор для архитектуры IA64, разработчики CLR поняли, что у многих программистов (включая их самих) есть код, который не будет корректно работать при вызове не из временных (неупорядоченных) операций записи и чтения. Поэтому они решили, что JIT-компилятор IA64 должен всегда создавать инструкции чтения, включающие семантику запроса, а операции записи должны всегда включать семантику освобождения. Это позволило существующим приложениям, работающим в архитектуре х8б, работать без ошибок и в архитектуре IA64. К сожалению, это сильно ударило бы по производительности, поэтому пришлось пойти на компромисс. Во всех операциях записи в JIT-компиляторе IА64 используется семантика освобождения, а чтение выполняется в обычном порядке. Чтобы выполнить чтение с семантикой запроса, нужно вызвать метод VolatileRead или применить ключевое слово volatile. Это намного более понятная модель управления памятью, которую разработчики вполне в состоянии освоить. Microsoft обещает, что все имеющиеся и любые будущие JIT-компиляторы будут четко придерживаться этой модели памяти, в которой используются обычные операции чтения, но запись всегда выполняется с семантикой освобождения. Если быть совсем точным, правила доступа к памяти немного сложнее. Впрочем, большинство программистов об этом догадывается. Примечание: Как говорилось в главе 1, ассоциация ЕСМА определила стандартную версию CLR, названную Common Language Infrastructure (CLI). В документации ЕСМА описывается модель памяти, которой должны соответствовать все CLI-совместимые среды времени выполнения. В этой версии модели памяти все операции чтения и записи являются неупорядоченными, если только программист явно не вызывает метод VolatileRead или VolatileWrite или не использует ключевое слово volatile. Это также объясняет, почему FCL все еще содержит методы VolatileWrite, хотя они бесполезны, если приложение работает в CLR, созданной компанией Microsoft. В Microsoft посчитали модель ЕСМА слишком сложной и непонятной, поэтому реализовали собственную версию CLI (то есть CLR), использующую более сильную модель памяти, в которой все операции записи выполняются с семантикой освобождения. Такое изменение позволяет считать CLR компании Microsoft совместимой со стандартом, так как любые приложения, написанные для модели памяти ЕСМА, могут работать в CLR от Microsoft. Однако существует возможная проблема, заключающаяся в том, что кто-то может написать приложение и протестировать его при помощи CLR компании Microsoft. Это приложение может отлично работать, но в другой реализации CLI оно может не работать. Было бы лучше для всех, если бы стандарт ЕСМА и все реализации этого стандарта придерживались единой модели памяти. Microsoft надеется, что в следующих версиях стандарта ЕСМА будет принята модель памяти, которая сейчас применяется в CLR. Наконец, нужно сказать, что всегда, когда поток вызывает Interlocked-метод (см. следующий раздел), процессор принудительно обеспечивает согласованность кеша. Так что при работе с переменными при помощи Interlocked-методов не нужно беспокоиться о соответствии этой модели памяти. Кроме того, все блокировки синхронизации потоков (Monitor, ReaderWriterLock,Mutex, Semaphore, AutoResetEvent, ManualResetEvent и другие) вызывают Interlocked-мегоды, так что программисту также не приходится заботиться о моделях памяти. Внимание! В общем случае, я настоятельно не рекомендую вызывать методы VolatileRead и VolatileWrite, а также ключевое слово volatile. Вместо этого лучше использовать Interlocked-методы или конструкции синхронизации потоков более высокого уровня. Эти методы работают всегда, независимо от модели памяти и платформы процессора.
|