Решение 11 распространенных проблем в многопоточном коде - Разрыв чтения и записи

ОГЛАВЛЕНИЕ

Разрыв чтения и записи

Как уже упоминалось ранее, щадящие состязания позволяют получать доступ к переменным без синхронизации. Чтение и запись выровненных слов естественного размера, например подходящих под величину указателя переменных, имеющих размер 32 бита (4 байта) на 32-разрядных процессорах и 64 бита (8 байт) на 64-разрядных процессорах, являются атомарными. Если поток просто считывает единственную переменную, которую какой-нибудь другой поток запишет, и не затрагивается никаких сложных инвариантов, порой эта гарантия позволяет полностью пропустить синхронизацию.

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

К примеру, представьте себе, что ThreadA находится в цикле и записывает только 0x0L и 0xaaaabbbbccccddddL в 64-разрядную переменную s_x. ThreadB находится в читающем ее цикле (см. рис. 2).

 Рис. 2. Разрыв готов произойти

internal static volatile long s_x;
void ThreadA() {
  int i = 0;
  while (true) {
    s_x = (i & 1) == 0 ? 0x0L : 0xaaaabbbbccccddddL;
    i++;
  }
}
void ThreadB() {
  while (true) {
    long x = s_x;
    Debug.Assert(x == 0x0L || x == 0xaaaabbbbccccddddL);
  }
}

Читатель может быть удивлен тем, что подтверждение ThreadB готово сработать. Причина этого состоит в том, что запись в потоке ThreadA будет состоять из двух частей, старшей 32-разрядной и младшей 32-разрядной, в некотором порядке, зависящем от компилятора. То же касается считывания в потоке ThreadB. Таким образом, поток ThreadB может обнаружить значения 0xaaaabbbb00000000L или 0x00000000aaaabbbbL.