Safe locks for multi-thread applications(多线程应用程序的安全锁) ...

打印 上一主题 下一主题

主题 571|帖子 571|积分 1713

Safe locks for multi-thread applications(多线程应用程序的安全锁)

由AB4327-GANDI,2016年1月9日。永世链接
开源mORMot框架
一旦你的应用程序是多线程的,就应该保护并发数据访问。我们已经写过关于调试多线程应用程序可能很困难的文章。
否则,可能会出现“竞态条件”问题:例如,如果两个线程同时修改一个变量(例如减少计数器),值可能会变得不一致且不安全。逻辑错误的另一个症状是“死锁”,当两个线程错误地利用锁时,会导致整个应用程序好像被阻塞且无响应,从而相互阻塞。
在预期24/7运行且无需维护的服务器体系上,应避免此类问题。
在Delphi中,资源(可能是一个对象或任何变量)的保护通常通过临界区来实现。
临界区是一个对象,用于确保代码的一部分一次只能由一个线程实行。临界区需要在利用之前创建/初始化,并在不再需要时释放。然后,一些代码通过利用Enter/Leave方法举行保护,这将锁定其实行:实际上,只有一个线程会拥有临界区,所以只有一个线程可以大概实行这段代码,其他线程将等待直到锁被释放。为了获得最佳性能,受保护的地区应尽可能小——否则,利用线程的好处可能会失效,因为任何其他线程都会等待拥有临界区的线程释放锁。
我们现在将看到Delphi的 TCriticalSection可能存在的问题,以及我们的框架提出简化临界区在您的应用程序中的利用。
:在Delphi中,TCriticalSection 是用于管理线程同步的一个类。当多个线程需要访问共享资源时,可以利用 TCriticalSection 来确保每次只有一个线程可以访问该资源,从而防止数据竞争和不一致。然而,TCriticalSection 的利用也可能带来一些问题,比如死锁大概性能瓶颈,因此需要谨慎利用。mORMot框架提供了一些工具和策略来简化 TCriticalSection 的利用,并资助开发者更安全、更有用地管理线程同步。
修复 TRTLCriticalSection

在实践中,您可能会利用一个 TCriticalSection类,大概更低级别的 TRTLCriticalSection记录,后者可能是更好的选择,因为它利用的内存更少,并且可以很容易地作为任何 class定义的(受保护)字段包含进去。
假设我们要保护对变量a和b的任何访问。以下是如何利用临界区方法来实现:
  1. var CS: TRTLCriticalSection;
  2.     a, b: integer;
  3. // 在线程开始前设置
  4. InitializeCriticalSection(CS);
  5. // 在每个TThread.Execute中:
  6. EnterCriticalSection(CS);
  7. try // 通过try...finally块保护锁
  8.   // 从现在开始,您可以安全地更改变量
  9.   inc(a);
  10.   inc(b);
  11. finally
  12.   // 安全块结束
  13.   LeaveCriticalSection(CS);
  14. end;
  15. // 当线程停止时
  16. DeleteCriticalSection(CS);
复制代码
在最新版本的Delphi中,您可以利用 TMonitor类,它允许任何Delphi TObject拥有锁。
在XE5之前,存在一些性能问题,即使到现在,这个受Java启发的特性可能也不是最佳方法,因为它与单个对象绑定,并且与较旧版本的Delphi(或FPC)不兼容。
几年前,Eric Grange报告说——拜见这篇博客文章——TRTLCriticalSection(连同 TMonitor)存在严峻的设计缺陷,进入/离开不同的临界区可能会使您的线程序列化,甚至整个性能可能比线程被序列化时更差。这是因为它是一个小的、动态分配的对象,所以几个 TRTLCriticalSection的内存可能最终会落在同一个CPU缓存行中,当发生这种情况时,运行线程的焦点之间会发生大量的缓存冲突。
Eric提出的修复方法非常简单:
  1. type
  2.    TFixedCriticalSection = class(TCriticalSection)
  3.    private
  4.      FDummy: array [0..95] of Byte;
  5.    end;
复制代码
从T*Locked继承

在定义您自己的类时,您可以继承一些提供 TSynLocker实例的类,如在 SynCommons.pas中定义的:
  1.   TSynPersistentLocked = class(TSynPersistent)
  2.   ...
  3.     property Safe: TSynLocker read fSafe;
  4.   end;
  5.   TInterfacedObjectLocked = class(TInterfacedObjectWithCustomCreate)
  6.   ...
  7.     property Safe: TSynLocker read fSafe;
  8.   end;
  9.   TObjectListLocked = class(TObjectList)
  10.   ...
  11.     property Safe: TSynLocker read fSafe;
  12.   end;
  13.   TRawUTF8ListHashedLocked = class(TRawUTF8ListHashed)
  14.   ...
  15.     property Safe: TSynLocker read fSafe;
  16.   end;
复制代码
所有这些类都将在其 constructor/destructor中初始化和闭幕它们所拥有的 Safe实例。
因此,我们可以如许编写我们的类:
  1. type
  2.   TMyClass = class(TSynPersistentLocked)
  3.   protected
  4.     fField: integer;
  5.   public
  6.     procedure UseLockUnlock;
  7.     procedure UseProtectMethod;
  8.   end;
  9. { TMyClass }
  10. procedure TMyClass.UseLockUnlock;
  11. begin
  12.   fSafe.Lock;
  13.   try
  14.     // 现在我们可以安全地从多个线程访问任何受保护的字段
  15.     inc(fField);
  16.   finally
  17.     fSafe.UnLock;
  18.   end;
  19. end;
  20. procedure TMyClass.UseProtectMethod;
  21. begin
  22.   fSafe.ProtectMethod; // 调用fSafe.Lock并返回IUnknown本地实例
  23.   // 现在我们可以安全地从多个线程访问任何受保护的字段
  24.   inc(fField);
  25.   // 当IUnknown被释放时,将调用fSafe.UnLock
  26. end;
复制代码
如您所见,Safe: TSynLocker实例将在 TSynPersistentLocked父级定义并处理。
注入IAutoLocker实例

如果您的类继承自 TInjectableObject,您甚至可以定义以下内容:
  1. type
  2.   TMyClass = class(TInjectableObject)
  3.   private
  4.     fLock: IAutoLocker;
  5.     fField: integer;
  6.   public
  7.     function FieldValue: integer;
  8.   published
  9.     property Lock: IAutoLocker read fLock write fLock;
  10.   end;
  11. { TMyClass }
  12. function TMyClass.FieldValue: integer;
  13. begin
  14.   Lock.ProtectMethod;
  15.   result := fField;
  16.   inc(fField);
  17. end;
  18. var c: TMyClass;
  19. begin
  20.   c := TMyClass.CreateInjected([],[],[]);
  21.   Assert(c.FieldValue=0);
  22.   Assert(c.FieldValue=1);
  23.   c.Free;
  24. 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中:
  1. type
  2.   TMyClass = class(TSynPersistentLocked)
  3.   public
  4.     procedure UseInternalIncrement;
  5.     function FieldValue: integer;
  6.   end;
  7. { TMyClass }
  8. function TMyClass.FieldValue: integer;
  9. begin // 值的读取也将受到互斥锁的保护
  10.   result := fSafe.LockedInt64[0];
  11. end;
  12. procedure TMyClass.UseInternalIncrement;
  13. begin // 这个专用的方法将确保原子增加
  14.   fSafe.LockedInt64Increment(0,1);
  15. end;
复制代码
请注意,我们利用了 TSynLocker.LockedInt64Increment()方法,因为以下方式是不安全的:
  1. procedure TMyClass.UseInternalIncrement;
  2. begin
  3.   fSafe.LockedInt64[0] := fSafe.LockedInt64[0]+1;
  4. end;
复制代码
在上面的代码中,获取了两个锁(每个 LockedInt64属性调用一个),因此另一个线程可能会在两者之间修改值,并且增量可能不如预期准确。
TSynLocker提供了一些专用的属性和方法来处理这种安全的存储。这些期望一个 Index值,范围从 0..6:
  1.     property Locked[Index: integer]: Variant read GetVariant write SetVariant;
  2.     property LockedInt64[Index: integer]: Int64 read GetInt64 write SetInt64;
  3.     property LockedPointer[Index: integer]: Pointer read GetPointer write SetPointer;
  4.     property LockedUTF8[Index: integer]: RawUTF8 read GetUTF8 write SetUTF8;
  5.     function LockedInt64Increment(Index: integer; const Increment: Int64): Int64;
  6.     function LockedExchange(Index: integer; const Value: variant): variant;
  7.     function LockedPointerExchange(Index: integer; Value: pointer): pointer;
复制代码
如果有须要,您可以存储一个 pointer或对 TObject实例的引用。
在我们的框架中,提供如许一套线程安全的方法是有意义的,该框架提供了多线程服务器能力——请参阅线程安全性
请随时在mORMot文档上继续阅读,其中可能包含有关此主题的更新和附加信息。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

傲渊山岳

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表