161人参与 • 2024-06-01 • Delphi
由ab4327-gandi,2016年1月9日。永久链接
一旦你的应用程序是多线程的,就应该保护并发数据访问。我们已经写过关于调试多线程应用程序可能很困难的文章。
否则,可能会出现“竞态条件”问题:例如,如果两个线程同时修改一个变量(例如减少计数器),值可能会变得不一致且不安全。逻辑错误的另一个症状是“死锁”,当两个线程错误地使用锁时,会导致整个应用程序似乎被阻塞且无响应,从而相互阻塞。
在预期24/7运行且无需维护的服务器系统上,应避免此类问题。
在delphi中,资源(可能是一个对象或任何变量)的保护通常通过临界区来实现。
临界区是一个对象,用于确保代码的一部分一次只能由一个线程执行。临界区需要在使用之前创建/初始化,并在不再需要时释放。然后,一些代码通过使用enter/leave方法进行保护,这将锁定其执行:实际上,只有一个线程会拥有临界区,所以只有一个线程能够执行这段代码,其他线程将等待直到锁被释放。为了获得最佳性能,受保护的区域应尽可能小——否则,使用线程的好处可能会失效,因为任何其他线程都会等待拥有临界区的线程释放锁。
我们现在将看到delphi的 tcriticalsection
可能存在的问题,以及我们的框架提出简化临界区在您的应用程序中的使用。
注:在delphi中,tcriticalsection
是用于管理线程同步的一个类。当多个线程需要访问共享资源时,可以使用 tcriticalsection
来确保每次只有一个线程可以访问该资源,从而防止数据竞争和不一致。然而,tcriticalsection
的使用也可能带来一些问题,比如死锁或者性能瓶颈,因此需要谨慎使用。mormot框架提供了一些工具和策略来简化 tcriticalsection
的使用,并帮助开发者更安全、更有效地管理线程同步。
在实践中,您可能会使用一个 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;
在定义您自己的类时,您可以继承一些提供 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
父级定义并处理。
如果您的类继承自 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
继承以进行自身的依赖解析,因此这个技巧可能非常方便。
当我们解决了潜在的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文档上继续阅读,其中可能包含有关此主题的更新和附加信息。
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论