С точки зрения производительности на процессорах Intel это то же самое, но для простоты, чтобы было проще понять, я предпочитаю первый способ из приведенных вами примеров.Нет никакой причины использовать cmpxchg
для получения блокировки, если вы можете сделать это с xchg
.
Согласно принципу бритвы Оккама, простые вещи лучше.
Кроме того, блокировка с помощью xchg
более мощна - вы также можете проверить правильность логики вашего программного обеспечения, т.е.что вы не обращаетесь к байту памяти, который не был явно выделен для блокировки, или что вы не разблокируете дважды.
Нет единого мнения о том, должно ли снятие блокировки быть обычным хранилищем или lock
-ед магазин.Например, LeaveCriticalSection в Windows 10 использует lock
-ed-хранилище для снятия блокировки даже на процессоре с одним сокетом;в то время как на нескольких физических процессорах с неоднородным доступом к памяти (NUMA) проблема снятия блокировки: обычное хранилище против хранилища с lock
может быть даже более важной.
См. этоПример более безопасных функций блокировки, которые проверяют данные на достоверность и отлавливают попытки снять блокировки, которые не были получены:
const
cLockAvailable = 107; // arbitrary constant, use any unique values that you like, I've chosen prime numbers
cLockLocked = 109;
cLockFinished = 113;
function AcquireLock(var Target: LONG): Boolean;
var
R: LONG;
begin
R := InterlockedExchange(Target, cLockByteLocked);
case R of
cLockAvailable: Result := True; // we've got a value that indicates that the lock was available, so return True to the caller indicating that we have acquired the lock
cLockByteLocked: Result := False; // we've got a value that indicates that the lock was already acquire by someone else, so return False to the caller indicating that we have failed to acquire the lock this time
else
begin
raise Exception.Create('Serious application error - tried to acquire lock using a variable that has not been properly initialized');
end;
end;
end;
procedure ReleaseLock(var Target: LONG);
var
R: LONG;
begin
// As Peter Cordes pointed out (see comments below), releasing the lock doesn't have to be interlocked, just a normal store. Even for debugging we use normal load. However, Windows 10 uses locked release on LeaveCriticalSection.
R := Target;
Target := cLockAvailable;
if R <> cLockByteLocked then
begin
raise Exception.Create('Serious application error - tried to release a lock that has not been actually locked');
end;
end;
Ваше основное приложение находится здесь:
var
AreaLocked: LONG;
begin
AreaLocked := cLockAvailable; // on program initialization, fill the default value
....
if AcquireLock(AreaLocked) then
try
// do something critical with the locked area
...
finally
ReleaseLock(AreaLocked);
end;
....
AreaLocked := cLockFinished; // on program termination, set the special value to catch probable cases when somebody will try to acquire the lock
end.
Вы также можете использоватьследующий код в виде спин-цикла, он использует нормальную загрузку при вращении для экономии ресурсов, как это предложил Питер Кордес.После 5000 циклов он вызывает функцию API Windows SwitchToThread ().Это значение 5000 циклов является моим эмпирическим.Значения от 500 до 50000 также кажутся нормальными, в некоторых случаях более низкие значения лучше, а в других - лучше.Обратите внимание, что вы можете использовать этот код только на процессорах, которые поддерживают SSE2 - вы должны проверить соответствующий бит CPUID перед вызовом инструкции pause
- в противном случае будет просто потеря энергии.На процессорах без pause
просто используйте другие средства, такие как EnterCriticalSection / LeaveCriticalSection или Sleep (0), а затем Sleep (1) в цикле.Некоторые люди говорят, что на 64-битных процессорах вы можете не проверять SSE2, чтобы убедиться, что инструкция pause
реализована, потому что оригинальная архитектура AMD64 приняла SSE Intel и SSE2 в качестве основных инструкций, и, практически, если вы запускаете 64-битовый код, у вас уже есть SSE2 и, следовательно, инструкция pause
.Тем не менее, Intel не рекомендует полагаться на особую функцию присутствия и прямо заявляет, что определенная функция может исчезнуть в будущих процессорах, и приложения всегда должны проверять функции через CPUID.Однако инструкции SSE стали вездесущими, и многие 64-разрядные компиляторы используют их без проверки (например, Delphi для Win64), поэтому шансы на то, что в некоторых будущих процессорах не будет SSE2, не говоря уже о pause
, очень малы.
// on entry rcx = address of the byte-lock
// on exit: al (eax) = old value of the byte at [rcx]
@Init:
mov edx, cLockByteLocked
mov r9d, 5000
mov eax, edx
jmp @FirstCompare
@DidntLock:
@NormalLoadLoop:
dec r9
jz @SwitchToThread // for static branch prediction, jump forward means "unlikely"
pause
@FirstCompare:
cmp [rcx], al // we are using faster, normal load to not consume the resources and only after it is ready, do once again interlocked exchange
je @NormalLoadLoop // for static branch prediction, jump backwards means "likely"
lock xchg [rcx], al
cmp eax, edx // 32-bit comparison is faster on newer processors like Xeon Phi or Cannonlake.
je @DidntLock
jmp @Finish
@SwitchToThread:
push rcx
call SwitchToThreadIfSupported
pop rcx
jmp @Init
@Finish: