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

17

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

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

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

6 ответов

18

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

    
ответ дан Mehrdad Afshari 27.02.2009 в 19:48
источник
12

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

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

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

ответ дан Jon Skeet 27.02.2009 в 20:40
источник
7

MSDN :

  

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

Но также:

  

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

    
ответ дан eljenso 27.02.2009 в 19:51
источник
2

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

    
ответ дан codekaizen 27.02.2009 в 20:35
источник
1

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

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

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

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

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

    
ответ дан Ben S 27.02.2009 в 19:53
источник
0

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

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

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

    
ответ дан Mike 27.02.2009 в 19:53
источник