Pull to refresh

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

Reading time3 min
Views9.5K
Буквально на днях ко мне обратились с вопросом.

А зачем нужен префикс 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 в частности, иногда может вам дать гораздо больше информации, чем предположения о том как оно работает на самом деле и это факт, но…



Нет, это не факт, это больше чем факт — так оно и было на самом деле
Tags:
Hubs:
Total votes 14: ↑7 and ↓70
Comments2

Articles