我读过的所有内容都表明TRTTIContext是线程安全的。
然而,当多线程时,TRTTIContext.FindType似乎偶尔会失败(返回nil)。在它周围使用TCriticalSection可以解决问题。请注意,我使用的是XE6,这个问题似乎并不存在于XE中。 编辑:似乎存在于所有具有新RTTI单元的Delphi版本中。
我已经完成了一个你可以用来亲眼看看的测试项目。创建一个新的VCL项目,删除TMemo和TButton,用下面替换unit1,并分配Form1.OnCreate,Form1.OnDestroy和Button1.OnClick事件。密钥CS是TTestThread.Execute中的GRTTIBlock。目前已禁用,当我运行200个线程时,我得到3到5个失败。启用GRTTIBlock CS可以消除故障。
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, SyncObjs, Contnrs, RTTI;
type
TTestThread = class(TThread)
private
FFailed: Boolean;
FRan: Boolean;
FId: Integer;
protected
procedure Execute; override;
public
property Failed: Boolean read FFailed;
property Ran: Boolean read FRan;
property Id: Integer read FId write FId;
end;
TForm1 = class(TForm)
Memo1: TMemo;
Button1: TButton;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FThreadBlock: TCriticalSection;
FMaxThreadCount: Integer;
FThreadCount: Integer;
FRanCount: Integer;
FFailureCount: Integer;
procedure Log(AStr: String);
procedure ThreadFinished(Sender: TObject);
procedure LaunchThreads;
end;
var
Form1: TForm1;
implementation
var
GRTTIBlock: TCriticalSection;
{$R *.dfm}
{ TTestThread }
procedure TTestThread.Execute;
var
ctx : TRTTIContext;
begin
// GRTTIBlock.Acquire;
try
FFailed := not Assigned(ctx.FindType('Unit1.TForm1'));
FRan := True;
finally
// GRTTIBlock.Release;
end;
end;
{ TForm1 }
procedure TForm1.Button1Click(Sender: TObject);
begin
Randomize;
LaunchThreads;
Log(Format('Threads: %d, Ran: %d, Failures: %d',
[FMaxThreadCount, FRanCount, FFailureCount]));
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FThreadBlock := TCriticalSection.Create;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FThreadBlock.Free;
end;
procedure TForm1.Log(AStr: String);
begin
Memo1.Lines.Add(AStr);
end;
procedure TForm1.ThreadFinished(Sender: TObject);
var
tt : TTestThread;
begin
tt := TTestThread(Sender);
Log(Format('Thread %d finished', [tt.Id]));
FThreadBlock.Acquire;
try
Dec(FThreadCount);
if tt.Failed then
Inc(FFailureCount);
if tt.Ran then
Inc(FRanCount);
finally
FThreadBlock.Release;
end;
end;
procedure TForm1.LaunchThreads;
var
c : Integer;
ol : TObjectList;
t : TTestThread;
begin
FRanCount := 0;
FFailureCount := 0;
FMaxThreadCount := 200;
ol := TObjectList.Create(False);
try
// get all the thread objects created and ready
for c := 1 to FMaxThreadCount do
begin
t := TTestThread.Create(True);
t.FreeOnTerminate := True;
t.OnTerminate := ThreadFinished;
t.Id := c;
ol.Add(t);
end;
FThreadCount := FMaxThreadCount;
// start them all up
for c := 0 to ol.Count - 1 do
begin
TTestThread(ol[c]).Start;
Log(Format('Thread %d started', [TTestThread(ol[c]).Id]));
end;
repeat
Application.ProcessMessages;
FThreadBlock.Acquire;
try
if FThreadCount <= 0 then
Break;
finally
FThreadBlock.Release;
end;
until False;
finally
ol.Free;
end;
end;
initialization
GRTTIBlock := TCriticalSection.Create;
finalization
GRTTIBlock.Free;
end.
答案 0 :(得分:13)
我想我发现了这个问题。它位于TRealPackage.FindType
和MakeTypeLookupTable
内。
MakeTypeLookupTable
检查是否已分配FNameToType
。如果没有,则运行DoMake
。这个受TMonitor保护,并在进入后再次分配FNameToType
。
到目前为止一切顺利。但随后发生错误,因为内部DoMake
FNameToType
被分配,导致其他线程愉快地传递MakeTypeLookupTable
并返回FindType
,然后在FNameToType.TryGetValue
中返回false并且返回nil。
修复(希望对于XE8?):
由于在锁定的FNameToType
之外使用DoMake
作为执行可以继续的指示,因此在正确填写之前不应在DoMake
内分配。
答案 1 :(得分:9)
正如Stefan所解释的那样,问题在于双重检查锁定模式的错误实现。我想扩大他的答案,并试着让它更清楚是什么问题。
错误的代码如下所示:
procedure TRealPackage.MakeTypeLookupTable;
procedure DoMake;
begin
TMonitor.Enter(Flock);
try
if FNameToType <> nil then // presumes double-checked locking ok
Exit;
FNameToType := TDictionary<string,PTypeInfo>.Create;
// .... code removed from snippet that populates FNameToType
finally
TMonitor.Exit(Flock);
end;
end;
begin
if FNameToType <> nil then
Exit;
DoMake;
end;
错误是填充共享资源FNameToType
的代码在分配FNameToType
后执行。填充共享资源的代码需要在分配FNameToType
之前执行。
考虑两个线程A和B.它们是第一个调用MakeTypeLookupTable
的线程。线程A首先到达,发现FNameToType
为nil
并调用DoMake
。线程A获取锁并到达分配FNameToType
的代码。现在,在线程A设法运行更多代码之前,线程B到达MakeTypeLookupTable
。它测试FNameToType
并发现它不是nil
,因此立即返回。然后,调用代码使用FNameToType
。但是,FNameToType
尚不适合使用。它尚未填充,因为线程A尚未返回。
Embarcadero方面最明显的解决方案如下:
procedure DoMake;
var
LNameToType: TDictionary<string,PTypeInfo>;
begin
TMonitor.Enter(Flock);
try
if FNameToType <> nil then // presumes double-checked locking ok
Exit;
LNameToType := TDictionary<string,PTypeInfo>.Create;
// .... populate LNameToType
FNameToType := LNameToType;
finally
TMonitor.Exit(Flock);
end;
end;
但是,请注意假设双重检查锁定确定的评论。好吧,当机器具有足够强大的内存模型时,双重检查锁定就可以了。所以它在x86和x64上都很好。但ARM的内存模型相对较弱。所以我对这个修复程序是否足以支持ARM有很强的疑虑。事实上,我确实想知道Embarcadero使用双重检查锁定的RTL中的其他地方。
如果在代码的接口部分声明了TRealPackage
,那么修补TRealPackage.MakeTypeLookupTable
以应用上述更改就很容易了。但事实并非如此。因此,为了应用一种解决方法,我建议如下:
TRealPackage.MakeTypeLookupTable
。因为初始化是单线程的,所以你可以避免竞争条件。声明这样的全局上下文,比如说:
var
ctx: TRttiContext;
并强制拨打TRealPackage.MakeTypeLookupTable
这样的电话:
ctx.FindType('');
只要你的所有RTTI代码通过这个单独的共享上下文,那么你就不会违反这场比赛。