Временная запись и чтение, volatile-поля

29 Авг
2011

Современные процессоры, для повышения производительности, при обращении к памяти кэширует строки памяти. Доступ к кэш-памяти процессора осуществляется чрезвычайно быстро, особенно в сравнении с доступом к оперативной памяти на материнской плате или к жесткому диску. В основном повышение производительности достигается, из-за того что процессор считывает сразу и соседние биты данных, по отношению к запрашиваемым – это называют кэш строкой. Соседние биты считываются, потому что обычно информация хранится последовательно в памяти, а значит большая вероятность, того что приложение будет запрашивать и соседние биты, таким образом при запросе этих битов, они будут уже в кэше процессора, что и значительно сократит время поиска и доступа к данным. Таким образом, достигается повышение производительности.
Теперь рассмотрим приложение, которое запускается, на двух процессорном, или двухъядерном процессоре (с раздельной кэш памятью). Наше тестовое приложение имеет 2 потока, допусти это _thread0 и _thread1.
 internal class CacheCoherency {<br>
 private Thread _thread0;<br>
 private Thread _thread1;<br>
private Byte _init = 0;<br>
 private Int _value = 0;<br>
// Выполняется 1 потоком (_thread0)<br>
 public void Thread0Proc() {<br>
 _value = 5;<br>
 _init = 1;<br>
 }<br>
// Выполняется 2 потоком (_thread1)<br>
 public void Thread1Proc() {<br>
 if (_init == 1)<br>
 Console.WriteLine(_value);<br>
 } <br>
 }<br>

Рассмотрим выполнение этого приложения следующим образом:

— Второй процессор, считывает байт из памяти, находящийся рядом с байтом _value, а значит, строка памяти вместе с _value попадает в кэш второго процессора;
— Начинает выполняться поток _thread0, тем самым присваивая байту _value значение 5, выполнение потока продолжается, тем самым переменной _init присваивается значение 1, и значение может быть сброшено в оперативную память;
— Начинает свое выполнение поток _thread1, процессор запрашивает, значение _init, так как этого значение в кэше второго процессора нет, значение будет запрошено из оперативной памяти, и будет равно единице, что удовлетворяет наше условие. Далее поток продолжит выполнение, и запросит, значение _value, но так как оно уже есть в кэш памяти второго процессора, то на дисплей будет выведено «0»!
Подведя итог, можно сказать, что хотя кэширование и повышает производительность, но оно может привести к тому что, несколько потоков могут использовать разные значения одной и той же переменной. Это не относиться к однопроцессорным системам, так как они имеют только один процессорный кэш.
Согласованность значений в кэше и оперативной памяти – это серьезная проблема. Лучше всего, реализовывать методы, которые не будут иметь доступ к общим значениям из разных потоков, но иногда особого выбора нет.
Как с этим бороться?

Приведенные методы решения предназначены для среды CLR, но имеют аналоги и в других языках программирования. К примеру ключевое слово volatile есть и в языке С++, то есть ничего дополнительно реализовывать нет необходимости.
Временная запись и чтение
Среда CLR (.NET) предоставляет определенный контроль, над согласованием кэша. Класс System.Threading.Thread предлагает несколько статических методов, которые выглядят примерно так:
static Object VolatileRead(ref Object address);
static Byte VolatileRead(ref Byte address);
static SByte VolatileRead(ref SBbyte 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 UIntPtr 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, UIntPtr 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 обеспечивает защиту памяти— он сбрасывает данные процессорного кэша в основную память, после чего объявляет кеш недействительным
Давайте, переделаем наше приложение CacheCoherency на основе методов Volatile.
 internal class CacheCoherency {<br>
 private Thread _thread0;<br>
 private Thread _thread1;<br>
private Byte _init = 0;<br>
 private Int _value = 0;<br>
// Выполняется 1 потоком (_thread0)<br>
 public void Thread0Proc() {<br>
 _value = 5;<br>
 Thread.VolatileWrite(ref _init, 1);<br>
 }<br>
// Выполняется 2 потоком (_thread1)<br>
 public void Thread1Proc() {<br>
 if (Thread.VolatileRead(ref _init) == 1)<br>
 Console.WriteLine(_value);<br>
 } <br>
 }<br>

volatile-поля
Разработчикам сложно запомнить обо всех фоновых потоках, и прогнозировать использование данных разными потоками. А также всегда корректно вызывать методы VolatileRead и VolatileWrite. Среда CLR предоставляет оператор volatile, который можно применять к простым статическим типам (полям), таким как Byte, SByte, Int16, UInt16, Int32, UInt32, Char, Single и тд. Ключевое слово volatile в среде CLR гарантирует, что обращение к volatile-полям будут происходить по технологии временного считывания и записи, так что не обязательно явно вызывать какие либо статические методы Volatile. Фактически volatile указывает процессору, что не нужно кэшировать эти данные, и гарантирует, что значение будет считано из оперативной памяти.
Переделаем наше тестовое приложение, с учетом ключевого слова volatile.
 internal class CacheCoherency {<br>
 private Thread _thread0;<br>
 private Thread _thread1;<br>
private volatile Byte _init = 0;<br>
 private Int _value = 0;<br>
// Выполняется 1 потоком (_thread0)<br>
 public void Thread0Proc() {<br>
 _value = 5;<br>
 _init = 1;<br>
 }<br>
// Выполняется 2 потоком (_thread1)<br>
 public void Thread1Proc() {<br>
 if (_init == 1)<br>
 Console.WriteLine(_value);<br>
 } <br>
 }<br>

Таким образом, вы можете использовать данные решения для синхронизации кэш памяти процессора с оперативной памятью.
По материалам Хабрахабр.



загрузка...

Комментарии:

Наверх