Safe locks for multi-thread applications(多线程应用程序的安全锁)
Safe locks for multi-thread applications(多线程应用程序的安全锁)
由AB4327-GANDI,2016年1月9日。永久链接
一旦你的应用程序是多线程的,就应该保护并发数据访问。我们已经写过关于调试多线程应用程序可能很困难的文章。
否则,可能会出现“竞态条件”问题:例如,如果两个线程同时修改一个变量(例如减少计数器),值可能会变得不一致且不安全。逻辑错误的另一个症状是“死锁”,当两个线程错误地使用锁时,会导致整个应用程序似乎被阻塞且无响应,从而相互阻塞。
在预期24/7运行且无需维护的服务器系统上,应避免此类问题。
在Delphi中,资源(可能是一个对象或任何变量)的保护通常通过临界区来实现。
临界区是一个对象,用于确保代码的一部分一次只能由一个线程执行。临界区需要在使用之前创建/初始化,并在不再需要时释放。然后,一些代码通过使用Enter/Leave方法进行保护,这将锁定其执行:实际上,只有一个线程会拥有临界区,所以只有一个线程能够执行这段代码,其他线程将等待直到锁被释放。为了获得最佳性能,受保护的区域应尽可能小——否则,使用线程的好处可能会失效,因为任何其他线程都会等待拥有临界区的线程释放锁。
我们现在将看到Delphi的 TCriticalSection
可能存在的问题,以及我们的框架提出简化临界区在您的应用程序中的使用。
注:在Delphi中,TCriticalSection
是用于管理线程同步的一个类。当多个线程需要访问共享资源时,可以使用 TCriticalSection
来确保每次只有一个线程可以访问该资源,从而防止数据竞争和不一致。然而,TCriticalSection
的使用也可能带来一些问题,比如死锁或者性能瓶颈,因此需要谨慎使用。mORMot框架提供了一些工具和策略来简化 TCriticalSection
的使用,并帮助开发者更安全、更有效地管理线程同步。
修复 TRTLCriticalSection
在实践中,您可能会使用一个 TCriticalSection
类,或者更低级别的 TRTLCriticalSection
记录,后者可能是更好的选择,因为它使用的内存更少,并且可以很容易地作为任何 class
定义的(受保护)字段包含进去。
假设我们要保护对变量a和b的任何访问。以下是如何使用临界区方法来实现:
var CS: TRTLCriticalSection;
a, b: integer;
// 在线程开始前设置
InitializeCriticalSection(CS);
// 在每个TThread.Execute中:
EnterCriticalSection(CS);
try // 通过try...finally块保护锁
// 从现在开始,您可以安全地更改变量
inc(a);
inc(b);
finally
// 安全块结束
LeaveCriticalSection(CS);
end;
// 当线程停止时
DeleteCriticalSection(CS);
在最新版本的Delphi中,您可以使用 TMonitor
类,它允许任何Delphi TObject
拥有锁。
在XE5之前,存在一些性能问题,即使到现在,这个受Java启发的特性可能也不是最佳方法,因为它与单个对象绑定,并且与较旧版本的Delphi(或FPC)不兼容。
几年前,Eric Grange报告说——参见这篇博客文章——TRTLCriticalSection
(连同 TMonitor
)存在严重的设计缺陷,进入/离开不同的临界区可能会使您的线程序列化,甚至整个性能可能比线程被序列化时更差。这是因为它是一个小的、动态分配的对象,所以几个 TRTLCriticalSection
的内存可能最终会落在同一个CPU缓存行中,当发生这种情况时,运行线程的核心之间会发生大量的缓存冲突。
Eric提出的修复方法非常简单:
type
TFixedCriticalSection = class(TCriticalSection)
private
FDummy: array [0..95] of Byte;
end;
从T*Locked继承
在定义您自己的类时,您可以继承一些提供 TSynLocker
实例的类,如在 SynCommons.pas
中定义的:
TSynPersistentLocked = class(TSynPersistent)
...
property Safe: TSynLocker read fSafe;
end;
TInterfacedObjectLocked = class(TInterfacedObjectWithCustomCreate)
...
property Safe: TSynLocker read fSafe;
end;
TObjectListLocked = class(TObjectList)
...
property Safe: TSynLocker read fSafe;
end;
TRawUTF8ListHashedLocked = class(TRawUTF8ListHashed)
...
property Safe: TSynLocker read fSafe;
end;
所有这些类都将在其 constructor/destructor
中初始化和终结它们所拥有的 Safe
实例。
因此,我们可以这样编写我们的类:
type
TMyClass = class(TSynPersistentLocked)
protected
fField: integer;
public
procedure UseLockUnlock;
procedure UseProtectMethod;
end;
{ TMyClass }
procedure TMyClass.UseLockUnlock;
begin
fSafe.Lock;
try
// 现在我们可以安全地从多个线程访问任何受保护的字段
inc(fField);
finally
fSafe.UnLock;
end;
end;
procedure TMyClass.UseProtectMethod;
begin
fSafe.ProtectMethod; // 调用fSafe.Lock并返回IUnknown本地实例
// 现在我们可以安全地从多个线程访问任何受保护的字段
inc(fField);
// 当IUnknown被释放时,将调用fSafe.UnLock
end;
如您所见,Safe: TSynLocker
实例将在 TSynPersistentLocked
父级定义并处理。
注入IAutoLocker实例
如果您的类继承自 TInjectableObject
,您甚至可以定义以下内容:
type
TMyClass = class(TInjectableObject)
private
fLock: IAutoLocker;
fField: integer;
public
function FieldValue: integer;
published
property Lock: IAutoLocker read fLock write fLock;
end;
{ TMyClass }
function TMyClass.FieldValue: integer;
begin
Lock.ProtectMethod;
result := fField;
inc(fField);
end;
var c: TMyClass;
begin
c := TMyClass.CreateInjected([],[],[]);
Assert(c.FieldValue=0);
Assert(c.FieldValue=1);
c.Free;
end;
在这里,我们使用了依赖解析——请参阅[依赖注入和接口解析](http://synopse.info/files/html/Synopse mORMot Framework SAD 1.18.html#TITL_161)——让 TMyClass.CreateInjected
构造函数扫描其 published
属性,从而搜索 IAutoLocker
的提供者。由于 IAutoLocker
已全局注册为通过 TAutoLocker
解析,因此我们的类将使用新实例初始化其 fLock
字段。现在,我们可以像往常一样使用 Lock.ProtectMethod
来访问关联的 TSynLocker
临界区。
当然,这可能会比手动处理 TSynLocker
更复杂,但是如果您正在编写一个基于接口的服务,您的类可以从 TInjectableObject
继承以进行自身的依赖解析,因此这个技巧可能非常方便。
TSynLocker中的安全锁定存储
当我们解决了潜在的CPU缓存行问题时,您还记得我们在 TSynLocker
定义中添加了一个填充二进制缓冲区吗?由于我们不想浪费资源,TSynLocker
提供了对其内部数据的轻松访问,并允许直接处理这些值。由于它存储为7个 variant
值插槽,因此您可以存储任何类型的数据,包括复杂的 TDocVariant
文档或数组。
我们的类可以使用此功能,并将其整数字段值存储在内部插槽0中:
type
TMyClass = class(TSynPersistentLocked)
public
procedure UseInternalIncrement;
function FieldValue: integer;
end;
{ TMyClass }
function TMyClass.FieldValue: integer;
begin // 值的读取也将受到互斥锁的保护
result := fSafe.LockedInt64[0];
end;
procedure TMyClass.UseInternalIncrement;
begin // 这个专用的方法将确保原子增加
fSafe.LockedInt64Increment(0,1);
end;
请注意,我们使用了 TSynLocker.LockedInt64Increment()
方法,因为以下方式是不安全的:
procedure TMyClass.UseInternalIncrement;
begin
fSafe.LockedInt64[0] := fSafe.LockedInt64[0]+1;
end;
在上面的代码中,获取了两个锁(每个 LockedInt64
属性调用一个),因此另一个线程可能会在两者之间修改值,并且增量可能不如预期准确。
TSynLocker
提供了一些专用的属性和方法来处理这种安全的存储。这些期望一个 Index
值,范围从 0..6
:
property Locked[Index: integer]: Variant read GetVariant write SetVariant;
property LockedInt64[Index: integer]: Int64 read GetInt64 write SetInt64;
property LockedPointer[Index: integer]: Pointer read GetPointer write SetPointer;
property LockedUTF8[Index: integer]: RawUTF8 read GetUTF8 write SetUTF8;
function LockedInt64Increment(Index: integer; const Increment: Int64): Int64;
function LockedExchange(Index: integer; const Value: variant): variant;
function LockedPointerExchange(Index: integer; Value: pointer): pointer;
如果有必要,您可以存储一个 pointer
或对 TObject
实例的引用。
在我们的框架中,提供这样一套线程安全的方法是有意义的,该框架提供了多线程服务器能力——请参阅线程安全性。
请随时在mORMot文档上继续阅读,其中可能包含有关此主题的更新和附加信息。