В С# используется Int64 для 32-разрядного процессора

Я прочитал в документации MS, что назначение 64-битного значения на 32-разрядном компьютере Intel не является атомной операцией; то есть операция не является потокобезопасной. Это означает, что если два человека одновременно присваивают значение статическому полю Int64, конечное значение поля не может быть предсказано.

Вопрос с тремя частями:

  • Это правда?
  • Разве это то, о чем я буду беспокоиться в реальном мире?
  • Если мое приложение многопоточно, мне действительно нужно окружать все мои назначения Int64 кодом блокировки?

Ответ 1

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

Ответ 2

Даже если записи были атомарными, скорее всего, вам все равно придется вынимать блокировку всякий раз, когда вы обращаетесь к переменной. Если вы этого не сделали, вам нужно было бы, по крайней мере, сделать переменную volatile, чтобы убедиться, что все потоки увидели новое значение при следующем чтении переменной (что почти всегда то, что вы хотите). Это позволяет вам делать атомарные, изменчивые наборы, но как только вы захотите сделать что-нибудь более интересное, например добавить 5 к нему, вы вернетесь к блокировке.

Заблокировать свободное программирование очень и очень сложно. Вам нужно точно знать, что вы делаете, и сохранить сложность как можно меньшего фрагмента кода. Лично я редко даже пытаюсь попробовать, кроме как для очень известных шаблонов, таких как использование статического инициализатора для инициализации коллекции, а затем чтение из коллекции без блокировки.

Использование класса Interlocked может помочь в некоторых ситуациях, но почти всегда намного проще просто снять блокировку. Непротиворечивые блокировки "довольно дешевы" (по общему признанию, они становятся дорогими с большим количеством ядер, но так же и все) - не путайтесь с кодом без блокировки, пока не получите хорошее доказательство того, что оно действительно существенно изменится.

Ответ 3

MSDN:

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

Но также:

Как и любой другой тип, чтение и запись в общую переменную, которая содержит экземпляр этого типа, который должен защищаться замком, чтобы гарантировать безопасность потока.

Ответ 4

Если у вас есть общая переменная (скажем, как статическое поле класса или как поле совместно используемого объекта), и это поле или объект будет использоваться поперечно-нить, тогда да, вам нужно чтобы обеспечить доступ к этой переменной с помощью атомной операции. Процессор x86 имеет встроенные функции, чтобы убедиться, что это происходит, и это средство открывается через методы класса System.Threading.Interlocked.

Например:

class Program
{
    public static Int64 UnsafeSharedData;
    public static Int64 SafeSharedData;

    static void Main(string[] args)
    {
        Action<Int32> unsafeAdd = i => { UnsafeSharedData += i; };
        Action<Int32> unsafeSubtract = i => { UnsafeSharedData -= i; };
        Action<Int32> safeAdd = i => Interlocked.Add(ref SafeSharedData, i);
        Action<Int32> safeSubtract = i => Interlocked.Add(ref SafeSharedData, -i);

        WaitHandle[] waitHandles = new[] { new ManualResetEvent(false), 
                                           new ManualResetEvent(false),
                                           new ManualResetEvent(false),
                                           new ManualResetEvent(false)};

        Action<Action<Int32>, Object> compute = (a, e) =>
                                            {
                                                for (Int32 i = 1; i <= 1000000; i++)
                                                {
                                                    a(i);
                                                    Thread.Sleep(0);
                                                }

                                                ((ManualResetEvent) e).Set();
                                            };

        ThreadPool.QueueUserWorkItem(o => compute(unsafeAdd, o), waitHandles[0]);
        ThreadPool.QueueUserWorkItem(o => compute(unsafeSubtract, o), waitHandles[1]);
        ThreadPool.QueueUserWorkItem(o => compute(safeAdd, o), waitHandles[2]);
        ThreadPool.QueueUserWorkItem(o => compute(safeSubtract, o), waitHandles[3]);

        WaitHandle.WaitAll(waitHandles);
        Debug.WriteLine("Unsafe: " + UnsafeSharedData);
        Debug.WriteLine("Safe: " + SafeSharedData);
    }
}

Результаты:

Небезопасный: -24050275641 Безопасный: 0

В интересной заметке я запустил это в режиме x64 на Vista 64. Это показывает, что 64-битные поля обрабатываются как 32-битные поля по времени выполнения, то есть 64-разрядные операции неатомны. Кто-нибудь знает, является ли это проблемой CLR или проблемой x64?

Ответ 5

На 32-битной платформе x86 наибольшая часть памяти размером с атомный размер составляет 32 бит.

Это означает, что если что-то пишет или читает из 64-битной размерной переменной, это возможно для того, чтобы читать/записывать, чтобы получить предварительную эмулировку во время выполнения.

  • Например, вы начинаете присваивать значение 64-битной переменной.
  • После записи первых 32 бит ОС решает, что другой процесс получит процессорное время.
  • Следующий процесс пытается прочитать переменную, которой вы были в середине назначения.

Это всего лишь одно возможное условие гонки с 64-разрядным назначением на 32-битной платформе.

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

Ответ 6

Это правда? Да, как выясняется. Если в ваших регистрах содержится только 32 бита, и вам нужно сохранить 64-битное значение для некоторого места памяти, он будет выполнять две операции загрузки и две операции хранения. Если ваш процесс прерывается другим процессом между этими двумя загрузками/магазинами, другой процесс может испортить половину ваших данных! Удивительно, но факт. Это было проблемой для каждого процессора, когда-либо созданного - если ваш тип данных длиннее ваших регистров, у вас будут проблемы concurrency.

Разве это то, о чем я буду беспокоиться в реальном мире? Да и нет. Так как почти всем современным программам дается собственное адресное пространство, вам нужно будет только об этом беспокоиться, если вы выполняете многопоточное программирование.

Если мое приложение многопоточно, мне действительно нужно окружить все мои назначения Int64 кодом блокировки? К сожалению, да, если вы хотите получить техническую поддержку. Обычно на практике проще использовать Мьютекс или Семафор вокруг больших блоков кода, чем блокировать каждое отдельное задание множества для глобальных переменных.