Под C # опасно использование Int64 на 32-битном процессоре - PullRequest
17 голосов
/ 27 февраля 2009

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

Вопрос из трех частей:

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

Ответы [ 6 ]

18 голосов
/ 27 февраля 2009

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

12 голосов
/ 27 февраля 2009

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

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

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

7 голосов
/ 27 февраля 2009

MSDN :

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

Но также:

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

2 голосов
/ 27 февраля 2009

Если у вас есть общая переменная (скажем, как статическое поле класса или как поле общего объекта), и это поле или объект будет использоваться перекрестным потоком, то да, вам нужно чтобы убедиться, что доступ к этой переменной защищен с помощью атомарной операции. Процессор 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?

1 голос
/ 27 февраля 2009

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

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

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

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

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

0 голосов
/ 27 февраля 2009

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

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

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

...