Почему этот код не демонстрирует неатомичность операций чтения / записи? - PullRequest
11 голосов
/ 09 сентября 2010

Чтение этот вопрос , я хотел проверить, смогу ли я продемонстрировать неатомарность операций чтения и записи для типа, для которого атомарность таких операций не гарантируется.

private static double _d;

[STAThread]
static void Main()
{
    new Thread(KeepMutating).Start();
    KeepReading();
}

private static void KeepReading()
{
    while (true)
    {
        double dCopy = _d;

        // In release: if (...) throw ...
        Debug.Assert(dCopy == 0D || dCopy == double.MaxValue); // Never fails
    }
}

private static void KeepMutating()
{
    Random rand = new Random();
    while (true)
    {
        _d = rand.Next(2) == 0 ? 0D : double.MaxValue;
    }
}

К моему удивлению, утверждение отказывалось даже после полных трех минут исполнения.Что дает?

  1. Тест неверен.
  2. Специфические временные характеристики теста делают маловероятным / невозможным, чтобы утверждение провалилось.
  3. Вероятность настолько мала, чтоЯ должен выполнить тест намного дольше, чтобы вероятность того, что он сработает.
  4. CLR обеспечивает более строгие гарантии атомарности, чем спецификации C #.
  5. Моя ОС / аппаратное обеспечение обеспечивает более надежные гарантии, чемCLR.
  6. Что-то еще?

Конечно, я не собираюсь полагаться на какое-либо поведение, которое явно не гарантировано спецификацией, но я хотел бы получить более глубокое понимание

К вашему сведению, я выполнил это как для отладки, так и для выпуска (изменив Debug.Assert на if(..) throw) профилей в двух отдельных средах:

  1. Windows 7 64-разрядная+ .NET 3.5 SP1
  2. 32-разрядная Windows XP + .NET 2.0

РЕДАКТИРОВАТЬ: исключить возможность комментария Джона Кугельмана «отладчик не является безопасным по Шредингеру», посколькупроблема, я добавил линe someList.Add(dCopy); для метода KeepReading и убедитесь, что этот список не видит ни одного устаревшего значения из кэша.

РЕДАКТИРОВАТЬ: Основано на предложении Дэна Брайанта: использование long вместо double разрывовэто практически мгновенно.

Ответы [ 4 ]

12 голосов
/ 09 сентября 2010

Вы можете попробовать запустить его через CHESS , чтобы посмотреть, может ли оно вызвать чередование, которое нарушает тест.

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


РЕДАКТИРОВАТЬ: я пошел дальше и запустил разборки (принудительно цель x86). Соответствующие строки:

                double dCopy = _d;
00000039  fld         qword ptr ds:[00511650h] 
0000003f  fstp        qword ptr [ebp-40h]

                _d = rand.Next(2) == 0 ? 0D : double.MaxValue;
00000054  mov         ecx,dword ptr [ebp-3Ch] 
00000057  mov         edx,2 
0000005c  mov         eax,dword ptr [ecx] 
0000005e  mov         eax,dword ptr [eax+28h] 
00000061  call        dword ptr [eax+1Ch] 
00000064  mov         dword ptr [ebp-48h],eax 
00000067  cmp         dword ptr [ebp-48h],0 
0000006b  je          00000079 
0000006d  nop 
0000006e  fld         qword ptr ds:[002423D8h] 
00000074  fstp        qword ptr [ebp-50h] 
00000077  jmp         0000007E 
00000079  fldz 
0000007b  fstp        qword ptr [ebp-50h] 
0000007e  fld         qword ptr [ebp-50h] 
00000081  fstp        qword ptr ds:[00159E78h] 

Он использует один fstp qword ptr для выполнения операции записи в обоих случаях. Я предполагаю, что процессор Intel гарантирует атомарность этой операции, хотя я не нашел документации, подтверждающей это. Любой гуру x86, который может это подтвердить?


UPDATE:

Сбой, как и ожидалось, если вы используете Int64, который использует 32-разрядные регистры на процессоре x86, а не специальные регистры FPU. Вы можете увидеть это ниже:

                Int64 dCopy = _d;
00000042  mov         eax,dword ptr ds:[001A9E78h] 
00000047  mov         edx,dword ptr ds:[001A9E7Ch] 
0000004d  mov         dword ptr [ebp-40h],eax 
00000050  mov         dword ptr [ebp-3Ch],edx 

UPDATE:

Мне было любопытно, не получится ли это, если я принудительно выровняю не 8-байтовое выравнивание двойного поля в памяти, поэтому я собрал этот код:

    [StructLayout(LayoutKind.Explicit)]
    private struct Test
    {
        [FieldOffset(0)]
        public double _d1;

        [FieldOffset(4)]
        public double _d2;
    }

    private static Test _test;

    [STAThread]
    static void Main()
    {
        new Thread(KeepMutating).Start();
        KeepReading();
    }

    private static void KeepReading()
    {
        while (true)
        {
            double dummy = _test._d1;
            double dCopy = _test._d2;

            // In release: if (...) throw ...
            Debug.Assert(dCopy == 0D || dCopy == double.MaxValue); // Never fails
        }
    }

    private static void KeepMutating()
    {
        Random rand = new Random();
        while (true)
        {
            _test._d2 = rand.Next(2) == 0 ? 0D : double.MaxValue;
        }
    }

Это не дает сбоя, и сгенерированные инструкции x86 по существу такие же, как и раньше:

                double dummy = _test._d1;
0000003e  mov         eax,dword ptr ds:[03A75B20h] 
00000043  fld         qword ptr [eax+4] 
00000046  fstp        qword ptr [ebp-40h] 
                double dCopy = _test._d2;
00000049  mov         eax,dword ptr ds:[03A75B20h] 
0000004e  fld         qword ptr [eax+8] 
00000051  fstp        qword ptr [ebp-48h] 

Я экспериментировал с заменой _d1 и _d2 для использования с dCopy / set, а также пробовал FieldOffset, равный 2. Все сгенерировали одинаковые базовые инструкции (с различными смещениями выше), и все они не потерпели неудачу через несколько секунд (вероятно, миллиарды попыток) , Учитывая эти результаты, я осторожно уверен, что, по крайней мере, процессоры Intel x86 обеспечивают атомарность операций двойной загрузки / хранения независимо от выравнивания.

4 голосов
/ 09 сентября 2010

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

Чтобы предотвратить это, вам нужно либо синхронизировать доступ к _d (т.е. окружить его оператором lock), либо пометить _d как volatile.Если сделать его изменчивым, это скажет компилятору, что его значение может измениться в любое время, и поэтому оно никогда не должно кэшировать значение.

К сожалению (или к счастью), вы не можете пометить поле double как volatile, именно потому, чтоточки, которую вы пытаетесь проверить - атомный доступ к double невозможен!Синхронизация доступа к _d заставляет компилятор перечитать значение, но это также нарушает тест.О, хорошо!

2 голосов
/ 09 сентября 2010

Вы можете попробовать избавиться от 'dCopy = _d' и просто использовать _d в своем утверждении.

Таким образом, два потока читают / записывают в одну и ту же переменную одновременно.

Ваша текущая версия создает копию _d, которая создает новый экземпляр в одном потоке, что является поточно-безопасной операцией:

http://msdn.microsoft.com/en-us/library/system.double.aspx

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

Однако, если оба потока читают / записывают в один и тот же экземпляр переменной, тогда:

http://msdn.microsoft.com/en-us/library/system.double.aspx

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

Таким образом, если оба потока читают / записывают в один и тот же экземпляр переменной, вам потребуется блокировка для его защиты (или Interlocked.Read/Increment/Exchange., Не уверенный, работает ли он на удвоениях)

Редактировать

Как отмечают другие, чтение / запись на процессоре Intel двойное - это атомарная операция. Однако если программа скомпилирована для X86 и использует 64-битный целочисленный тип данных, операция не будет атомарной. Как показано в следующей программе. Замените Int64 на двойной, и он, кажется, работает.

    Public Const ThreadCount As Integer = 2
    Public thrdsWrite() As Threading.Thread = New Threading.Thread(ThreadCount - 1) {}
    Public thrdsRead() As Threading.Thread = New Threading.Thread(ThreadCount - 1) {}
    Public d As Int64

    <STAThread()> _
    Sub Main()

        For i As Integer = 0 To thrdsWrite.Length - 1

            thrdsWrite(i) = New Threading.Thread(AddressOf Write)
            thrdsWrite(i).SetApartmentState(Threading.ApartmentState.STA)
            thrdsWrite(i).IsBackground = True
            thrdsWrite(i).Start()

            thrdsRead(i) = New Threading.Thread(AddressOf Read)
            thrdsRead(i).SetApartmentState(Threading.ApartmentState.STA)
            thrdsRead(i).IsBackground = True
            thrdsRead(i).Start()

        Next

        Console.ReadKey()

    End Sub

    Public Sub Write()

        Dim rnd As New Random(DateTime.Now.Millisecond)
        While True
            d = If(rnd.Next(2) = 0, 0, Int64.MaxValue)
        End While

    End Sub

    Public Sub Read()

        While True
            Dim dc As Int64 = d
            If (dc <> 0) And (dc <> Int64.MaxValue) Then
                Console.WriteLine(dc)
            End If
        End While

    End Sub
0 голосов
/ 09 сентября 2010

IMO правильный ответ # 5.

double - это длина 8 байтов.

Интерфейс памяти - 64 бита = 8 байтов на модуль за такт (т.е. он становится 16 байтов для двойногопамять канала).

Есть также кэши ЦП.На моей машине строка кэша составляет 64 байта, а на всех процессорах она кратна 8.

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

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

...