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

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

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

Нет, это не факт, это больше чем факт — так оно и было на самом деле ©

— © Александр (Rouse_) Багель
Апрель, 2013

ссылка на оригинал статьи http://habrahabr.ru/post/181694/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *