Атомарные операции

    Буквально на днях ко мне обратились с вопросом.

    А зачем нужен префикс LOCK, или его аналог InterlockedDecrement при вызове процедуры _LStrClr из модуля System. Данная процедура декрементирует счетчик ссылок строки и при его обнулении освобождает память, ранее занятую строкой.

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

    В принципе предпосылка интересная, но…

    Но ведь мы передаем строку в класс нити.
    Это как минимум приводит к увеличению refCnt, а стало быть мы можем «попасть» на MemLeak в том случае, если бы не использовались атомарные операции при декременте счетчика ссылок.

    Это демонстрирует нам код _LStrClr:

    procedure _LStrClr(var S);
    {$IFDEF PUREPASCAL}
    var
      P: PStrRec;
    begin
      if Pointer(S) <> nil then
      begin
        P := Pointer(Integer(S) - Sizeof(StrRec));
        Pointer(S) := nil;
        if P.refCnt > 0 then
          if InterlockedDecrement(P.refCnt) = 0 then
            FreeMem(P);
      end;
    end;
    {$ELSE}
    asm
            { ->    EAX pointer to str      }
     
            MOV     EDX,[EAX]                       { fetch str                     }
            TEST    EDX,EDX                         { if nil, nothing to do         }
            JE      @@done
            MOV     dword ptr [EAX],0               { clear str                     }
            MOV     ECX,[EDX-skew].StrRec.refCnt    { fetch refCnt                  }
            DEC     ECX                             { if < 0: literal str           }
            JL      @@done
       LOCK DEC     [EDX-skew].StrRec.refCnt        { threadsafe dec refCount       }
            JNE     @@done
            PUSH    EAX
            LEA     EAX,[EDX-skew].StrRec.refCnt    { if refCnt now zero, deallocate}
            CALL    _FreeMem
            POP     EAX
    @@done:
    end;
    {$ENDIF}
    


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

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

    program interlocked;
     
    {$APPTYPE CONSOLE}
     
    uses
      Windows;
     
    const
      Limit = 1000000;
      DoubleLimit = Limit shl 1;
     
    var
      SameGlobalVariable: Integer;
     
    function Test1(lpParam: Pointer): DWORD; stdcall;
    var
      I: Integer;
    begin
      for I := 0 to Limit - 1 do
      asm
        lea eax, SameGlobalVariable
        inc [eax] // обычный инкремент
      end;
    end;
     
    function Test2(lpParam: Pointer): DWORD; stdcall;
    var
      I: Integer;
    begin
      for I := 0 to Limit - 1 do
      asm
        lea eax, SameGlobalVariable
        lock inc [eax] // атомарный инкремент
      end;
    end;
     
    var
      I: Integer;
      hThread: THandle;
      ThreadID: DWORD;
    begin
      // Неатомарное увеличение значения переменной SameGlobalVariable
      SameGlobalVariable := 0;
      hThread := CreateThread(nil, 0, @Test1, nil, 0, ThreadID);
      for I := 0 to Limit - 1 do
      asm
        lea eax, SameGlobalVariable
        inc [eax] // обычный инкремент
      end;
      WaitForSingleObject(hThread, INFINITE);
      CloseHandle(hThread);
      if SameGlobalVariable <> DoubleLimit then
        Writeln('Step one failed. Expected: ', DoubleLimit, ' but current: ', SameGlobalVariable);
     
      // Атомарное увеличение значения переменной SameGlobalVariable
      SameGlobalVariable := 0;
      hThread := CreateThread(nil, 0, @Test2, nil, 0, ThreadID);
      for I := 0 to Limit - 1 do
      asm
        lea eax, SameGlobalVariable
        lock inc [eax] // атомарный инкремент
      end;
      WaitForSingleObject(hThread, INFINITE);
      CloseHandle(hThread);
      if SameGlobalVariable <> DoubleLimit then
        Writeln('Step two failed. Expected: ', DoubleLimit, ' but current: ', SameGlobalVariable);
     
      Readln;
    end.
    


    Суть примера — есть некая глобальная переменная SameGlobalVariable (она выступает в роли счетчика ссылок строки из изначальной постановки задачи) и выполняются изменения ее значения в обычном и атомарном режимах с использованием нити.

    Здесь наглядно видно различия между двумя режимами работы.
    В консоли вы увидите примерно следующее:

    Step one failed. Expected: 2000000 but current: 1018924
    

    Ошибки по второму варианту реализации вы не увидите никогда.

    Кстати первый вариант может использоваться в качестве достаточно хорошего рандомизатора (о котором я говорил в предыдущих статьях).

    Резюмируя:

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



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

      В принципе предпосылка интересная, но…

      Что же тут интересного? Полностью ложная предпосылка.
      Все равно что сказать что невозможно представить что Земля крутится вокруг Солнца. Т.е. сказать такое конечно кто-то может, имея девственно чистый мозг, но вместо того чтобы доказывать ему обратное, надо отправить его изучать основы.
        0
        Что же тут интересного? Полностью ложная предпосылка.

        Ну вот видите — Вы это понимаете, но как это объяснить человеку, который ранее с этим не сталкивался?
        А сам вопрос — это дословная цитата, с которой все и началось :)

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое