Эта статья MSDN - только первый шаг в разработке многопоточных приложений: короче говоря, это означает «защитить ваши общие переменные с помощью блокировок (или критических разделов), потому что вы не уверены, что данные, которые вы читаете / пишете, являются одинаково для всех тем ".
Кэш процессора на ядро - лишь одна из возможных проблем, которая приведет к чтению неправильных значений. Еще одна проблема, которая может привести к состязанию, - это одновременная запись в ресурс двух потоков: невозможно определить, какое значение будет сохранено позже.
Поскольку код ожидает, что данные будут согласованными, некоторые многопоточные программы могут работать неправильно. При многопоточности вы не уверены, что код, который вы пишете с помощью отдельных инструкций, выполняется должным образом, когда он имеет дело с общими переменными.
InterlockedExchange/InterlockedIncrement
функции - это низкоуровневые коды операций asm с префиксом LOCK (или блокированные по конструкции, такие как код операции XCHG EDX,[EAX]
), которые действительно выльются в когерентность кэша для всех ядер ЦП и, следовательно, заставляют выполнение кода операции asm потокобезопасный.
Например, вот как реализован счетчик ссылок на строки, когда вы присваиваете строковое значение (см. _LStrAsg
в System.pas - это от нашей оптимизированной версии RTL для Delphi 7/2002 - поскольку оригинальный код Delphi защищен авторским правом):
MOV ECX,[EDX-skew].StrRec.refCnt
INC ECX { thread-unsafe increment ECX = reference count }
JG @@1 { ECX=-1 -> literal string -> jump not taken }
.....
@@1: LOCK INC [EDX-skew].StrRec.refCnt { ATOMIC increment of reference count }
MOV ECX,[EAX]
...
Существует разница между первыми INC ECX
и LOCK INC [EDX-skew].StrRec.refCnt
- не только первые инкременты ECX и не переменная подсчета ссылок, но и первая не являются поточно-ориентированными, в то время как 2-й имеет префикс LOCK, поэтому будет быть потокобезопасным.
Кстати, этот префикс LOCK является одной из проблем многопоточного масштабирования в RTL - лучше с новыми процессорами, но все же не идеально.
Таким образом, использование критических разделов - это самый простой способ сделать код потокобезопасным:
var GlobalVariable: string;
GlobalSection: TRTLCriticalSection;
procedure TThreadOne.Execute;
var LocalVariable: string;
begin
...
EnterCriticalSection(GlobalSection);
LocalVariable := GlobalVariable+'a'; { modify GlobalVariable }
GlobalVariable := LocalVariable;
LeaveCriticalSection(GlobalSection);
....
end;
procedure TThreadTwp.Execute;
var LocalVariable: string;
begin
...
EnterCriticalSection(GlobalSection);
LocalVariable := GlobalVariable; { thread-safe read GlobalVariable }
LeaveCriticalSection(GlobalSection);
....
end;
Использование локальной переменной делает критическую секцию короче, поэтому ваше приложение будет лучше масштабироваться и использовать всю мощь ядер вашего процессора. Между EnterCriticalSection
и LeaveCriticalSection
будет работать только один поток: другие потоки будут ожидать вызова EnterCriticalSection
... Чем короче критическая секция, тем быстрее ваше приложение. Некоторые неправильно спроектированные многопоточные приложения могут работать медленнее, чем однопоточные!
И не забывайте, что если ваш код внутри критического раздела может вызвать исключение, вы всегда должны писать явный блок try ... finally LeaveCriticalSection() end;
, чтобы защитить снятие блокировки и предотвратить любую мертвую блокировку вашего приложения.
Delphi идеально поточно-ориентирован, если вы защищаете свои общие данные с помощью блокировки, то есть критического раздела. Помните, что даже переменные с подсчетом ссылок (например, строки) должны быть защищены, даже если в их функциях RTL есть LOCK: этот LOCK предназначен для правильного подсчета ссылок и предотвращения утечек памяти, но он не будет поточно-ориентированным , Чтобы сделать это как можно быстрее, см. Этот вопрос .
Цель InterlockExchange
и InterlockCompareExchange
- изменить значение переменной общего указателя. Вы можете видеть его как «облегченную» версию критической секции для доступа к значению указателя.
Во всех случаях писать рабочий многопоточный код непросто - это даже трудный , , как только что написал эксперт Delphi в своем блоге .
Вы должны либо написать простые потоки без каких-либо общих данных (сделать личную копию данных до запуска потока, либо использовать совместно используемые данные только для чтения - которые по своей сути поточнобезопасны), либо вызвать некоторые хорошо спроектированные и проверенные библиотеки - такие как http://otl.17slon.com - которые сэкономят вам много времени на отладку.