Атомарность и Interlocked

Инструкция является атомарной, если она выполняется как единая, неделимая команда. Строгая атомарность препятствует любой попытке вытеснения. В C# простое чтение или присвоение значения полю в 32 бита или менее является атомарным (для 32-битных CPU). Операции с большими полями не атомарны, так как являются комбинацией более чем одной операции чтения/записи:

class Atomicity { static int x; static int y; static long z;   static void Test() { long myLocal; x = 3; // Атомарная операция z = 3; // Не атомарная (z – 64-битная переменная) myLocal = z; // Не атомарная (z is 64 bits) y += x; // Не атомарная (операции чтения и записи) x++; // Не атомарная (операции чтения и записи) } }

Чтение и запись 64-битных полей не атомарны на 32-битных CPU, так при этом используются два 32-битных участка памяти. Если поток A читает 64-битное значение, в то время как поток B обновляет его, поток A может получить битовую комбинацию из старого и нового значений.

Унарные операторы типа x++ сначала читают переменную, затем обрабатывают ее, а потом записывают новое значение. Рассмотрим следующий класс:

class ThreadUnsafe { static int x = 1000;   static void Go() { for (int i = 0; i < 100; i++) x--; } }

Вы могли бы подумать, что если бы десять потоков одновременно выполняли Go, в результате x было бы равно 0. Однако такой гарантии нет, потому что возможна ситуация, когда один поток вытеснит другой после получения им текущего значения x, уменьшит его и запишет его назад (в результате первый поток продолжит работу с устаревшим значением x).

Один из путей решения таких проблем – обернуть неатомарные операции в блокировку. Блокировка фактически моделирует атомарность. Однако класс Interlocked предлагает более простое и быстрое решение для простых атомарных операций:

class Program { static long sum;   static void Main() // sum { // Простой increment/decrement: Interlocked.Increment(ref sum); // 1 Interlocked.Decrement(ref sum); // 0   // Сложение/вычитание: Interlocked.Add(ref sum, 3); // 3   // Чтение 64-битного поля: Console.WriteLine(Interlocked.Read(ref sum)); // 3   // Запись 64-битного поля после чтения предыдущего значения: Console.WriteLine(Interlocked.Exchange(ref sum, 10)); // 10   // Обновление поля только если оно соответствует // определенному значению(10): Interlocked.CompareExchange(ref sum, 123, 10); // 123 } }

Использование Interlocked вообще более эффективно, чем lock, так как при этом в принципе отсутствует блокировка – и соответствующие накладные расходы на временную приостановку потока.

Interlocked аналогично действует и при использовании из разных процессов – в отличие от оператора lock, который эффективен только в рамках потоков текущего процесса. Это может быть использовано, например, при чтении и записи в разделяемую память (shared memory).