仅按需从 TThread 动态初始化和调用 LoadLibrary 一次

Posted

技术标签:

【中文标题】仅按需从 TThread 动态初始化和调用 LoadLibrary 一次【英文标题】:Dynamically initialize and call LoadLibrary from a TThread by demand only once 【发布时间】:2015-12-21 17:14:30 【问题描述】:

我有一个 Delphi DLL,需要从我的主 UI 应用程序或工作线程中调用。

我不想每次调用 DLL 时都调用 LoadLibrary/FreeLibrary。但是,我也不想在我的应用程序初始化部分加载它。因为我可能在应用程序的生命周期内根本不使用 DLL。

所以我需要的是第一个调用者(线程或主 UI)来初始化和加载 DLL。 DLL 将在最终部分中卸载。我意识到我需要一些同步。所以我使用了一个关键部分,但我似乎无法让它工作。

只有 一个 线程应该尝试加载 DLL。如果失败,其他线程不应尝试一次又一次地加载 DLL。

同步未按预期工作! 有人可以提出原因吗?

MCVE:

program Project1;
$APPTYPE CONSOLE
uses
  Windows,
  SysUtils,
  Classes;

const
  MyDLL = 'MyDLL.dll';

type
  TDLLProcessProc = function(A: Integer): Integer; stdcall;

var
  DLLProc: TDLLProcessProc = nil;
  DLLModule: HMODULE = 0;
  DLLInitialized: Boolean = False;
  DLLInitialized_OK: Boolean = False;
  CS: TRTLCriticalSection;

procedure InitDLLByFirstCall;
begin
  if DLLModule = 0 then
  begin
    if DLLInitialized then Exit;
    EnterCriticalSection(CS);
    try
      if DLLInitialized then Exit;
      DLLInitialized := True;
      DLLModule := LoadLibrary(MyDLL);
      if DLLModule = 0 then RaiseLastWin32Error;
      DLLProc := GetProcAddress(DLLModule, 'Process');
      if @DLLProc = nil then RaiseLastWin32Error;
      DLLInitialized_OK := True;
    finally
      LeaveCriticalSection(CS);
    end;
  end;
end;

function DLLProcess(A: Integer): Integer;
begin
  InitDLLByFirstCall;
  if not DLLInitialized_OK then
    raise Exception.Create('DLL was not initialized OK');
  Result := DLLProc(A);
end;

type
  TDLLThread = class(TThread)
  private
    FNum: Integer;
  public
    constructor Create(CreateSuspended: Boolean; ANum: Integer);
    procedure Execute; override;
  end;

constructor TDLLThread.Create(CreateSuspended: Boolean; ANum: Integer);
begin
  FreeOnTerminate := True;
  FNum := ANum;
  inherited Create(CreateSuspended);
end;

procedure TDLLThread.Execute;
var
  RetValue: Integer;
begin
  try
    RetValue := DLLProcess(FNum);
    Sleep(0);
    Writeln('TDLLThread Result=> ' + IntToStr(RetValue));
  except
    on E: Exception do
    begin
      Writeln('TDLLThread Error: ' + E.Message);
    end;
  end;
end;

var
  I: Integer;

begin
  InitializeCriticalSection(CS);
  try
    // First 10 thread always fail!  
    for I := 1 to 10 do
      TDLLThread.Create(False, I);
    Readln;

    for I := 1 to 10 do
      TDLLThread.Create(False, I);
    Readln;
  finally
    DeleteCriticalSection(CS);
  end;
end. 

DLL:

library MyDLL;

uses
  Windows;

$R *.res        

function Process(A: Integer): Integer; stdcall;
begin
  Result := A;
end;

exports
  Process;

begin
  IsMultiThread := True;
end.

【问题讨论】:

“未按预期工作”是什么意思?预期的行为是什么?观察到的行为是什么?它们有何不同? 不要检查DLLModule = 0。删除DLLInitialized_OK。仅在初始化完成后设置DLLInitialized。 -1 表示没有真正尝试。 可以在线程执行前销毁CS。 @DavidHeffernan,不,不是。 对于ReadLn,如果在线程启动之前就返回了呢? 【参考方案1】:

您需要修改代码,使在InitDLLByFirstCall 开头检查的条件变量仅在所有初始化完成后设置。因此,DLL 句柄是一个糟糕的选择。

其次,您需要在临界区内外使用相同的条件变量 - 如果您为此使用DLLInitialized,那么DLLInitialized_OKDLLModule 都没有真正的用处。

为了让事情更容易推理,您应该尽量避免使用最少数量的变量。像下面这样的东西应该可以工作:

var
  DLLProc: TDLLProcessProc = nil;
  DLLInitialized: Boolean = False;
  CS: TRTLCriticalSection;

procedure InitDLLByFirstCall;
var
  DLLModule: HMODULE;
begin
  if DLLInitialized then
    Exit;

  EnterCriticalSection(CS);
  try
    if not DLLInitialized then
    try
      DLLModule := LoadLibrary(MyDLL);
      Win32Check(DLLModule <> 0);

      DLLProc := GetProcAddress(DLLModule, 'Process');
      Win32Check(Assigned(DLLProc));
    finally
      DLLInitialized := True;
    end;
  finally
    LeaveCriticalSection(CS);
  end;
end;

function DLLProcess(A: Integer): Integer;
begin
  InitDLLByFirstCall;
  if @DLLProc = nil then
    raise Exception.Create('DLL was not initialized OK');
  Result := DLLProc(A);
end;

如果您不想检查DLLProcess 内的函数地址,那么您也可以为DLLInitialized 变量使用整数或枚举,未初始化 使用不同的值, 失败成功

【讨论】:

我可以像之前一样持有全局 DLLModule 以供以后在 FreeLibrary 中使用吗? @zig:可以,但是如果模块在程序关闭之前一直处于加载状态,则没有真正的用处。无论如何,Windows 都会在进程终止后进行清理。 感谢@mghie,非常感谢您的努力。 Win32Check(DLLModule &lt;&gt; 0)Win32Check(@DLLProc &lt;&gt; nil) 是一个很好的习惯用法,可以进一步简化这段编写良好的代码 @DavidHeffernan:确实,这样更容易阅读。感谢您的建议,已合并。【参考方案2】:

您的双重检查锁定实施不正确。您先分配给DLLModule,然后再分配给DLLProc。所以DLLModule 可以为非零,而DLLProc 仍然为空。

你在锁外测试的变量必须在所有初始化完成后修改。

图案是这样的:

if not Initialised then begin
  Lock.Enter;
  if not Initialised then begin
    // Do initialisation
    Initialised := True; // after initialisation complete
  end;
  Lock.Leave;
end;

请记住,此处实现的双重检查锁定仅在强大的 x86 内存模型下才有效。如果您曾经将此代码移动到具有弱内存模型的硬件上,它将无法按实现方式工作。您需要实施障碍。可以做到,但并非完全无足轻重。

不过,双重检查锁定在这里毫无意义。删除它并使用单个关键部分保护所有内容。您正在启动线程,这是一项非常昂贵的任务。关键部分的潜在争用可以忽略不计。

【讨论】:

"使用临界区" 但我确实使用了临界区。 哦,没关系,按照自己的方式做,有所有不必要的复杂性 所以你建议阻止每次调用,每次调用 DLLProcess 时都会使用关键部分(即使是从主 UI 调用)?如果是,这就是我实际开始的方式,但我认为这将是一个非常糟糕的主意。 没有。启动线程时阻塞一次。不管怎样,你似乎已经下定决心了。看起来你更感兴趣的是你不理解的勺子喂代码,而不是真正理解这些问题。用勺子喂食对我来说没有任何回报。我会帮助您尝试了解您是否真的想要。 当你说“不。在你启动线程时阻止一次”——这对我来说没有任何意义,对不起我的愚蠢。

以上是关于仅按需从 TThread 动态初始化和调用 LoadLibrary 一次的主要内容,如果未能解决你的问题,请参考以下文章

iOS 按需资源 - 仅按需下载。调试

Vue按需加载提升用户体验

具有非阻塞 tcp 调用的 TThread

ES11(2020)Dynamic Import 动态引入

ES11(2020)Dynamic Import 动态引入

多版本动态库的变量测试