陈龙餐饮 發表於 2024-5-27 15:39:00

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

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