如何使Allocate Hand线程安全?

时间:2012-01-11 13:44:08

标签: delphi

VCL组件设计为仅从应用程序的主线程使用。对于视觉组件,这从未给我带来任何困难。但是,我有时希望能够使用后台线程中的TTimer等非可视组件。或者确实只是创建一个隐藏的窗口。由于依赖AllocateHwnd,这是不安全的。现在,AllocateHwnd不是线程安全的,我理解这是设计的。

是否有一个简单的解决方案允许我从后台线程中使用AllocateHwnd

3 个答案:

答案 0 :(得分:14)

这个问题可以这样解决:

  1. 获取或实施线程安全版AllocateHwndDeallocateHwnd
  2. 替换VCL这些功能的不安全版本。
  3. 对于第1项,我使用Primož Gabrijelcic's代码,如blog article所述。对于第2项,我只是使用在运行时修补代码的非常着名的技巧,并使用无条件JMP指令替换不安全例程的开头,将执行重定向到线程安全函数。

    将所有内容放在一起会产生以下单元。

    (* Makes AllocateHwnd safe to call from threads. For example this makes TTimer
       safe to use from threads.  Include this unit as early as possible in your
       .dpr file.  It must come after any memory manager, but it must be included
       immediately after that before any included unit has an opportunity to call
       Classes.AllocateHwnd. *)
    unit MakeAllocateHwndThreadsafe;
    
    interface
    
    implementation
    
    {$IF CompilerVersion >= 23}{$DEFINE ScopedUnitNames}{$IFEND}
    uses
      {$IFDEF ScopedUnitNames}System.SysUtils{$ELSE}SysUtils{$ENDIF},
      {$IFDEF ScopedUnitNames}System.Classes{$ELSE}Classes{$ENDIF},
      {$IFDEF ScopedUnitNames}Winapi.Windows{$ELSE}Windows{$ENDIF},
      {$IFDEF ScopedUnitNames}Winapi.Messages{$ELSE}Messages{$ENDIF};
    
    const //DSiAllocateHwnd window extra data offsets
      GWL_METHODCODE = SizeOf(pointer) * 0;
      GWL_METHODDATA = SizeOf(pointer) * 1;
    
      //DSiAllocateHwnd hidden window (and window class) name
      CDSiHiddenWindowName = 'DSiUtilWindow';
    
    var
      //DSiAllocateHwnd lock
      GDSiWndHandlerCritSect: TRTLCriticalSection;
      //Count of registered windows in this instance
      GDSiWndHandlerCount: integer;
    
    //Class message dispatcher for the DSiUtilWindow class. Fetches instance's WndProc from
    //the window extra data and calls it.
    function DSiClassWndProc(Window: HWND; Message, WParam, LParam: longint): longint; stdcall;
    var
      instanceWndProc: TMethod;
      msg            : TMessage;
    begin
      {$IFDEF CPUX64}
      instanceWndProc.Code := pointer(GetWindowLongPtr(Window, GWL_METHODCODE));
      instanceWndProc.Data := pointer(GetWindowLongPtr(Window, GWL_METHODDATA));
      {$ELSE}
      instanceWndProc.Code := pointer(GetWindowLong(Window, GWL_METHODCODE));
      instanceWndProc.Data := pointer(GetWindowLong(Window, GWL_METHODDATA));
      {$ENDIF ~CPUX64}
      if Assigned(TWndMethod(instanceWndProc)) then
      begin
        msg.msg := Message;
        msg.wParam := WParam;
        msg.lParam := LParam;
        msg.Result := 0;
        TWndMethod(instanceWndProc)(msg);
        Result := msg.Result
      end
      else
        Result := DefWindowProc(Window, Message, WParam,LParam);
    end; { DSiClassWndProc }
    
    //Thread-safe AllocateHwnd.
    //  @author  gabr [based on http://fidoforum.ru/pages/new46s35o217746.ru.delphi and
    //                 TIcsWndHandler.AllocateHWnd from ICS v6 (http://www.overbyte.be)]
    //  @since   2007-05-30
    function DSiAllocateHWnd(wndProcMethod: TWndMethod): HWND;
    var
      alreadyRegistered: boolean;
      tempClass        : TWndClass;
      utilWindowClass  : TWndClass;
    begin
      Result := 0;
      FillChar(utilWindowClass, SizeOf(utilWindowClass), 0);
      EnterCriticalSection(GDSiWndHandlerCritSect);
      try
        alreadyRegistered := GetClassInfo(HInstance, CDSiHiddenWindowName, tempClass);
        if (not alreadyRegistered) or (tempClass.lpfnWndProc <> @DSiClassWndProc) then begin
          if alreadyRegistered then
            {$IFDEF ScopedUnitNames}Winapi.{$ENDIF}Windows.UnregisterClass(CDSiHiddenWindowName, HInstance);
          utilWindowClass.lpszClassName := CDSiHiddenWindowName;
          utilWindowClass.hInstance := HInstance;
          utilWindowClass.lpfnWndProc := @DSiClassWndProc;
          utilWindowClass.cbWndExtra := SizeOf(TMethod);
          if {$IFDEF ScopedUnitNames}Winapi.{$ENDIF}Windows.RegisterClass(utilWindowClass) = 0 then
            raise Exception.CreateFmt('Unable to register DSiWin32 hidden window class. %s',
              [SysErrorMessage(GetLastError)]);
        end;
        Result := CreateWindowEx(WS_EX_TOOLWINDOW, CDSiHiddenWindowName, '', WS_POPUP,
          0, 0, 0, 0, 0, 0, HInstance, nil);
        if Result = 0 then
          raise Exception.CreateFmt('Unable to create DSiWin32 hidden window. %s',
                  [SysErrorMessage(GetLastError)]);
        {$IFDEF CPUX64}
        SetWindowLongPtr(Result, GWL_METHODDATA, NativeInt(TMethod(wndProcMethod).Data));
        SetWindowLongPtr(Result, GWL_METHODCODE, NativeInt(TMethod(wndProcMethod).Code));
        {$ELSE}
        SetWindowLong(Result, GWL_METHODDATA, cardinal(TMethod(wndProcMethod).Data));
        SetWindowLong(Result, GWL_METHODCODE, cardinal(TMethod(wndProcMethod).Code));
        {$ENDIF ~CPUX64}
        Inc(GDSiWndHandlerCount);
      finally LeaveCriticalSection(GDSiWndHandlerCritSect); end;
    end; { DSiAllocateHWnd }
    
    //Thread-safe DeallocateHwnd.
    //  @author  gabr [based on http://fidoforum.ru/pages/new46s35o217746.ru.delphi and
    //                 TIcsWndHandler.AllocateHWnd from ICS v6 (http://www.overbyte.be)]
    //  @since   2007-05-30
    procedure DSiDeallocateHWnd(wnd: HWND);
    begin
      if wnd = 0 then
        Exit;
      DestroyWindow(wnd);
      EnterCriticalSection(GDSiWndHandlerCritSect);
      try
        Dec(GDSiWndHandlerCount);
        if GDSiWndHandlerCount <= 0 then
          {$IFDEF ScopedUnitNames}Winapi.{$ENDIF}Windows.UnregisterClass(CDSiHiddenWindowName, HInstance);
      finally LeaveCriticalSection(GDSiWndHandlerCritSect); end;
    end; { DSiDeallocateHWnd }
    
    procedure PatchCode(Address: Pointer; const NewCode; Size: Integer);
    var
      OldProtect: DWORD;
    begin
      if VirtualProtect(Address, Size, PAGE_EXECUTE_READWRITE, OldProtect) then begin
        Move(NewCode, Address^, Size);
        FlushInstructionCache(GetCurrentProcess, Address, Size);
        VirtualProtect(Address, Size, OldProtect, @OldProtect);
      end;
    end;
    
    type
      PInstruction = ^TInstruction;
      TInstruction = packed record
        Opcode: Byte;
        Offset: Integer;
      end;
    
    procedure RedirectProcedure(OldAddress, NewAddress: Pointer);
    var
      NewCode: TInstruction;
    begin
      NewCode.Opcode := $E9;//jump relative
      NewCode.Offset := NativeInt(NewAddress)-NativeInt(OldAddress)-SizeOf(NewCode);
      PatchCode(OldAddress, NewCode, SizeOf(NewCode));
    end;
    
    initialization
      InitializeCriticalSection(GDSiWndHandlerCritSect);
      RedirectProcedure(@AllocateHWnd, @DSiAllocateHWnd);
      RedirectProcedure(@DeallocateHWnd, @DSiDeallocateHWnd);
    
    finalization
      DeleteCriticalSection(GDSiWndHandlerCritSect);
    
    end.
    

    此单元必须尽早包含在.dpr文件的单元列表中。显然它不能出现在任何自定义内存管理器之前,但它应该在此之后立即出现。原因是必须在调用AllocateHwnd之前安装替换例程。

    更新我已经合并了最新版本的Primož代码,他很友好地发给我。

答案 1 :(得分:7)

不要在线程中使用TTimer,它永远不会安全。有线程:

1)使用SetTimer()和手动消息循环。如果使用回调函数,则不需要HWND,但仍需要发送消息。

2)使用CreateWaitableTimer(),然后在循环中调用WaitForSingleObject(),直到发出定时器信号。

3)使用timeSetEvent(),这是一个多线程计时器。要小心,因为它的回调是在自己的线程中调用的,所以要确保你的回调函数是线程安全的,并且允许你在该线程内调用的内容有限制。最好让它设置一个信号,你的真实线程等待,然后在定时器之外工作。

答案 2 :(得分:2)

由于您已经编写了在专用线程中运行的代码,因此我假设您不希望在此代码等待某些内容时运行任何代码。在这种情况下,您可以使用特定的毫秒数或少量毫秒来调用Sleep,并在循环中使用它来检查Now或GetTickCount以查看是否已经过了一定时间。使用Sleep还会降低CPU使用率,因为操作系统会发出信号,表示您不需要线程继续运行。