Буквально на днях ко мне обратились с вопросом.
А зачем нужен префикс LOCK, или его аналог InterlockedDecrement при вызове процедуры _LStrClr из модуля System. Данная процедура декрементирует счетчик ссылок строки и при его обнулении освобождает память, ранее занятую строкой.
Суть вопроса была такова — практически невозможно представить себе ситуацию, когда строка потеряет рефы одновременно из двух нитей, а стало быть атомарная операция в данном случае избыточна.
В принципе предпосылка интересная, но…
Но ведь мы передаем строку в класс нити.
Это как минимум приводит к увеличению refCnt, а стало быть мы можем «попасть» на MemLeak в том случае, если бы не использовались атомарные операции при декременте счетчика ссылок.
Это демонстрирует нам код _LStrClr:
В случае использования неатомарного декремента инструкция JNE имеет огромный шанс выполниться не верно. (И она действительно выполнится не верно, если убрать LOCK префикс).
Я конечно пробовал объяснить данную ситуацию примерами из интеловского мануала, где объясняется работа, но в итоге решил реализовать следующий пример (которым и смог убедить автора вопроса):
Суть примера — есть некая глобальная переменная SameGlobalVariable (она выступает в роли счетчика ссылок строки из изначальной постановки задачи) и выполняются изменения ее значения в обычном и атомарном режимах с использованием нити.
Здесь наглядно видно различия между двумя режимами работы.
В консоли вы увидите примерно следующее:
Ошибки по второму варианту реализации вы не увидите никогда.
Кстати первый вариант может использоваться в качестве достаточно хорошего рандомизатора (о котором я говорил в предыдущих статьях).
Резюмируя:
Анализ исходного кода системных модулей Delphi и VCL в частности, иногда может вам дать гораздо больше информации, чем предположения о том как оно работает на самом деле и это факт, но…
Нет, это не факт, это больше чем факт — так оно и было на самом деле
А зачем нужен префикс 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 в частности, иногда может вам дать гораздо больше информации, чем предположения о том как оно работает на самом деле и это факт, но…
Нет, это не факт, это больше чем факт — так оно и было на самом деле